packages feed

moonlight-category-0.1.0.0: src-finite/Moonlight/Category/Pure/FinPresentation.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DerivingStrategies #-}

{-| A name-binding builder for finite categories that compiles to the validated
'FinCat' semantic representation.

The builder has two deliberately separate dialects:

* A finite-poset dialect. 'below' declares strict generating inequalities;
  transitive closure is computed and cyclic declarations are rejected. Identities
  are implicit, so the resulting category carries the corresponding reflexive
  order.

* A fully enumerated finite-category dialect. 'arrow' declares every nonidentity
  morphism, 'identityAt' denotes an identity, and 'equate' supplies the composition
  table. The 'FinCat' constructors remain the semantic owners that validate
  closure and associativity.

A longer path equation is accepted only when each proper intermediate composite can
already be resolved from other equations. This module does not construct quotients
of free categories by arbitrary path congruences.
-}
module Moonlight.Category.Pure.FinPresentation
  ( FinBuilder,
    ObjRef,
    ArrowExpr,
    FinCatBuildError (..),
    object,
    objects,
    arrow,
    identityAt,
    below,
    after,
    equate,
    finCategory,
  )
where

import Control.Applicative ((<|>))
import Data.Bifunctor (first)
import Data.Bits (bit, (.|.))
import Data.IntMap.Strict (IntMap)
import qualified Data.IntMap.Strict as IntMap
import Data.Kind (Type)
import Data.List.NonEmpty (NonEmpty)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe (catMaybes)
import Data.Sequence (Seq, ViewL (..), (|>))
import qualified Data.Sequence as Seq
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Vector (Vector)
import qualified Data.Vector as Vector
import Moonlight.Category.Pure.FinCat
  ( FinCat,
    FinCatValidationError,
    FinGeneratorId (..),
    FinMorphismId (..),
    FinObjectId (..),
    trustedFinCatWithGeneratorBasis,
    trustedDenseThinFinCatFromReachabilityRows,
  )
import Moonlight.Category.Pure.Finite.DenseReachability
  ( denseClosureCycleComponents,
    denseClosureReachabilityRows,
    denseReachabilityWithCycles,
  )

-- | An opaque reference to an object declared by 'object'.
type ObjRef :: Type
data ObjRef = ObjRef !FinObjectId !String
  deriving stock (Eq, Show)

-- | An opaque morphism expression. Expressions are identities, named nonidentity
-- morphisms, or composites built with 'after'.
type ArrowExpr :: Type
data ArrowExpr
  = ExprIdentity !FinObjectId !String
  | ExprGen !FinGeneratorId !FinObjectId !FinObjectId !String
  | ExprComp !ArrowExpr !ArrowExpr
  deriving stock (Eq, Ord)

instance Show ArrowExpr where
  showsPrec precedence expression =
    case expression of
      ExprIdentity _ objectName ->
        showString "id[" . showString objectName . showChar ']'
      ExprGen _ _ _ arrowName ->
        showString arrowName
      ExprComp leftExpression rightExpression ->
        showParen
          (precedence > 9)
          ( showsPrec 10 leftExpression
              . showString " `after` "
              . showsPrec 9 rightExpression
          )

-- | Faults discovered while building or compiling a presentation.
type FinCatBuildError :: Type
data FinCatBuildError
  = DuplicateObjectName String
  | DuplicateArrowName String
  | DanglingObjectReference FinObjectId
  | MixedPresentationModes
  | CyclicStrictOrder [FinObjectId]
  | NonComposablePath ArrowExpr ArrowExpr FinObjectId FinObjectId
  | NonParallelEquation
      ArrowExpr
      ArrowExpr
      (FinObjectId, FinObjectId)
      (FinObjectId, FinObjectId)
  | UnsupportedEquation ArrowExpr ArrowExpr
  | UnresolvedComposite ArrowExpr
  | ConflictingComposition ArrowExpr ArrowExpr ArrowExpr ArrowExpr
  | IdentityEquationMismatch ArrowExpr ArrowExpr ArrowExpr
  | BuilderPatternFailure String
  | InvalidPresentation (NonEmpty FinCatValidationError)
  deriving stock (Eq, Show)

type ArrowDecl :: Type
data ArrowDecl =
  ArrowDecl !FinGeneratorId !FinObjectId !FinObjectId

type BuilderState :: Type
data BuilderState = BuilderState
  { builderObjectIds :: !(Map String FinObjectId),
    builderNextObject :: !Int,
    builderArrows :: !(Map String ArrowDecl),
    builderNextArrow :: !Int,
    builderBelowEdgesRev :: ![(FinObjectId, FinObjectId)],
    builderEquationsRev :: ![(ArrowExpr, ArrowExpr)],
    builderFirstError :: !(Maybe FinCatBuildError)
  }

initialState :: BuilderState
initialState =
  BuilderState
    { builderObjectIds = Map.empty,
      builderNextObject = 0,
      builderArrows = Map.empty,
      builderNextArrow = 0,
      builderBelowEdgesRev = [],
      builderEquationsRev = [],
      builderFirstError = Nothing
    }

-- | A pure presentation builder. The 'Maybe' lets refutable @do@-patterns
-- (e.g. @[a, b, c] <- objects [..]@) short-circuit via 'fail' while still
-- preserving the declarations accumulated so far for error reporting.
type FinBuilder :: Type -> Type
newtype FinBuilder a = FinBuilder {unFinBuilder :: BuilderState -> (Maybe a, BuilderState)}

instance Functor FinBuilder where
  fmap f (FinBuilder run) =
    FinBuilder (\state -> let (result, state') = run state in (fmap f result, state'))

instance Applicative FinBuilder where
  pure value = FinBuilder (\state -> (Just value, state))
  FinBuilder runF <*> FinBuilder runA =
    FinBuilder
      ( \state ->
          case runF state of
            (Nothing, state') -> (Nothing, state')
            (Just f, state') ->
              let (result, state'') = runA state'
               in (fmap f result, state'')
      )

instance Monad FinBuilder where
  FinBuilder run >>= k =
    FinBuilder
      ( \state ->
          case run state of
            (Nothing, state') -> (Nothing, state')
            (Just value, state') -> unFinBuilder (k value) state'
      )

instance MonadFail FinBuilder where
  fail message =
    FinBuilder (\state -> (Nothing, recordError (BuilderPatternFailure message) state))

recordError :: FinCatBuildError -> BuilderState -> BuilderState
recordError buildError state =
  case builderFirstError state of
    Nothing -> state {builderFirstError = Just buildError}
    Just _ -> state
{-# INLINE recordError #-}

objectReferenceDeclared :: BuilderState -> FinObjectId -> Bool
objectReferenceDeclared state (FinObjectId objectIndex) =
  objectIndex >= 0 && objectIndex < builderNextObject state
{-# INLINE objectReferenceDeclared #-}

recordObjectReference :: FinObjectId -> BuilderState -> BuilderState
recordObjectReference objectId state =
  if objectReferenceDeclared state objectId
    then state
    else recordError (DanglingObjectReference objectId) state
{-# INLINE recordObjectReference #-}

-- | Declare an object, returning an opaque reference for subsequent declarations.
object :: String -> FinBuilder ObjRef
object objectName =
  FinBuilder
    ( \state ->
        case Map.lookup objectName (builderObjectIds state) of
          Just objectId ->
            ( Just (ObjRef objectId objectName),
              recordError (DuplicateObjectName objectName) state
            )
          Nothing ->
            let objectId = FinObjectId (builderNextObject state)
                nextState =
                  state
                    { builderObjectIds =
                        Map.insert
                          objectName
                          objectId
                          (builderObjectIds state),
                      builderNextObject =
                        builderNextObject state + 1
                    }
             in (Just (ObjRef objectId objectName), nextState)
    )

-- | Declare several objects in source order.
objects :: [String] -> FinBuilder [ObjRef]
objects = traverse object

-- | Declare a named nonidentity morphism. Every nonidentity morphism of a general
-- presentation must be declared explicitly.
arrow :: ObjRef -> ObjRef -> String -> FinBuilder ArrowExpr
arrow (ObjRef sourceId _) (ObjRef targetId _) arrowName =
  FinBuilder
    ( \state ->
        case Map.lookup arrowName (builderArrows state) of
          Just (ArrowDecl generatorId existingSource existingTarget) ->
            ( Just
                ( ExprGen
                    generatorId
                    existingSource
                    existingTarget
                    arrowName
                ),
              recordError (DuplicateArrowName arrowName) state
            )
          Nothing ->
            let checkedState =
                  recordObjectReference targetId
                    (recordObjectReference sourceId state)
                generatorId =
                  FinGeneratorId (builderNextArrow checkedState)
                nextState =
                  checkedState
                    { builderArrows =
                        Map.insert
                          arrowName
                          (ArrowDecl generatorId sourceId targetId)
                          (builderArrows checkedState),
                      builderNextArrow =
                        builderNextArrow checkedState + 1
                    }
             in ( Just
                    (ExprGen generatorId sourceId targetId arrowName),
                  nextState
                )
    )

-- | The identity morphism at a declared object.
identityAt :: ObjRef -> ArrowExpr
identityAt (ObjRef objectId objectName) =
  ExprIdentity objectId objectName
{-# INLINE identityAt #-}

-- | Declare a strict generating inequality. Cyclic strict inequalities are
-- rejected; identities and transitive consequences are supplied by compilation.
below :: ObjRef -> ObjRef -> FinBuilder ()
below (ObjRef sourceId _) (ObjRef targetId _) =
  FinBuilder
    ( \state ->
        let checkedState =
              recordObjectReference targetId
                (recordObjectReference sourceId state)
         in ( Just (),
              checkedState
                { builderBelowEdgesRev =
                    (sourceId, targetId)
                      : builderBelowEdgesRev checkedState
                }
            )
    )

-- | @g `after` f@ denotes @g ∘ f@: first @f@, then @g@.
infixr 9 `after`

after :: ArrowExpr -> ArrowExpr -> ArrowExpr
after = ExprComp
{-# INLINE after #-}

-- | Record an equation between parallel morphism expressions.
equate :: ArrowExpr -> ArrowExpr -> FinBuilder ()
equate leftExpression rightExpression =
  FinBuilder
    ( \state ->
        ( Just (),
          state
            { builderEquationsRev =
                (leftExpression, rightExpression)
                  : builderEquationsRev state
            }
        )
    )

-- | Compile a presentation to a validated finite category.
finCategory :: FinBuilder a -> Either FinCatBuildError FinCat
finCategory builder =
  case unFinBuilder builder initialState of
    (_, state) ->
      case builderFirstError state of
        Just buildError -> Left buildError
        Nothing -> compilePresentation state

compilePresentation :: BuilderState -> Either FinCatBuildError FinCat
compilePresentation state =
  let objectCount = builderNextObject state
      objectSet =
        Set.fromDistinctAscList
          (FinObjectId <$> [0 .. objectCount - 1])
      belowEdges =
        reverse (builderBelowEdgesRev state)
      arrows =
        Map.elems (builderArrows state)
      equations =
        reverse (builderEquationsRev state)
      hasBelow =
        not (null belowEdges)
      hasGeneral =
        not (null arrows) || not (null equations)
   in case (hasBelow, hasGeneral) of
        (True, True) ->
          Left MixedPresentationModes
        (True, False) ->
          compileStrictOrder objectCount objectSet belowEdges
        (False, _) ->
          compileGeneral objectCount objectSet arrows equations

compileStrictOrder ::
  Int ->
  Set FinObjectId ->
  [(FinObjectId, FinObjectId)] ->
  Either FinCatBuildError FinCat
compileStrictOrder objectCount objectSet belowEdges =
  case
    danglingEndpoints
      objectCount
      (belowEdges >>= \(sourceId, targetId) -> [sourceId, targetId])
    of
      badObject : _ ->
        Left (DanglingObjectReference badObject)
      [] ->
        let closure =
              denseReachabilityWithCycles
                (importRowsFromEdges objectCount belowEdges)
            closedRows =
              denseClosureReachabilityRows closure
            cycleComponents =
              denseClosureCycleComponents closure
         in if null cycleComponents
              then
                Right
                  ( trustedDenseThinFinCatFromReachabilityRows
                      objectSet
                      closedRows
                  )
              else
                Left
                  (CyclicStrictOrder (objectIdsFromComponents cycleComponents))

compileGeneral ::
  Int ->
  Set FinObjectId ->
  [ArrowDecl] ->
  [(ArrowExpr, ArrowExpr)] ->
  Either FinCatBuildError FinCat
compileGeneral objectCount objectSet arrows equations =
  case danglingEndpoints objectCount (arrowEndpointIds arrows) of
    badObject : _ ->
      Left (DanglingObjectReference badObject)
    [] ->
      case firstDanglingEquationObject objectCount equations of
        Just badObject ->
          Left (DanglingObjectReference badObject)
        Nothing -> do
          compositionMap <- compileEquations equations
          let generatorBasis =
                Set.fromList (arrowMorphismId <$> arrows)
          first
            InvalidPresentation
            ( trustedFinCatWithGeneratorBasis
                generatorBasis
                objectSet
                (morphismMapFromArrows arrows)
                compositionMap
            )

arrowEndpointIds :: [ArrowDecl] -> [FinObjectId]
arrowEndpointIds =
  foldr
    ( \(ArrowDecl _ sourceId targetId) rest ->
        sourceId : targetId : rest
    )
    []

arrowMorphismId :: ArrowDecl -> FinMorphismId
arrowMorphismId (ArrowDecl generatorId _ _) =
  FinGeneratorMorphismId generatorId

morphismMapFromArrows ::
  [ArrowDecl] ->
  Map (FinObjectId, FinObjectId) [FinMorphismId]
morphismMapFromArrows =
  foldl' insertArrow Map.empty
  where
    insertArrow morphismMap (ArrowDecl generatorId sourceId targetId) =
      Map.insertWith
        (<>)
        (sourceId, targetId)
        [FinGeneratorMorphismId generatorId]
        morphismMap

type CompositionKey :: Type
type CompositionKey =
  (FinMorphismId, FinMorphismId)

type OrientedEquation :: Type
data OrientedEquation =
  OrientedEquation !ArrowExpr !ArrowExpr !FinMorphismId

type CompositionClaim :: Type
data CompositionClaim =
  CompositionClaim !ArrowExpr !ArrowExpr !FinMorphismId

type BlockedExpression :: Type
data BlockedExpression =
  BlockedExpression !CompositionKey !ArrowExpr

type EquationAttempt :: Type
data EquationAttempt
  = EquationBlocked !BlockedExpression
  | EquationSatisfied
  | EquationClaims !CompositionKey !CompositionClaim

type WaitingEquation :: Type
data WaitingEquation =
  WaitingEquation !ArrowExpr !OrientedEquation

compileEquations ::
  [(ArrowExpr, ArrowExpr)] ->
  Either FinCatBuildError (Map CompositionKey FinMorphismId)
compileEquations equations = do
  preparedEquations <- traverse prepareEquation equations
  claims <-
    solveEquations
      Map.empty
      Map.empty
      (Seq.fromList (catMaybes preparedEquations))
  pure (fmap compositionClaimResultId claims)

prepareEquation ::
  (ArrowExpr, ArrowExpr) ->
  Either FinCatBuildError (Maybe OrientedEquation)
prepareEquation (leftExpression, rightExpression) = do
  leftEndpoints <- expressionEndpoints leftExpression
  rightEndpoints <- expressionEndpoints rightExpression
  if leftEndpoints == rightEndpoints
    then orientParallelEquation leftExpression rightExpression
    else
      Left
        ( NonParallelEquation
            leftExpression
            rightExpression
            leftEndpoints
            rightEndpoints
        )

orientParallelEquation ::
  ArrowExpr ->
  ArrowExpr ->
  Either FinCatBuildError (Maybe OrientedEquation)
orientParallelEquation leftExpression rightExpression
  | leftExpression == rightExpression =
      Right Nothing
  | otherwise =
      case
        ( leftExpression,
          rightExpression,
          atomicMorphismId leftExpression,
          atomicMorphismId rightExpression
        )
        of
          (ExprComp _ _, _, _, Just resultId) ->
            Right
              ( Just
                  ( OrientedEquation
                      leftExpression
                      rightExpression
                      resultId
                  )
              )
          (_, ExprComp _ _, Just resultId, _) ->
            Right
              ( Just
                  ( OrientedEquation
                      rightExpression
                      leftExpression
                      resultId
                  )
              )
          (_, _, Just leftId, Just rightId)
            | leftId == rightId ->
                Right Nothing
          _ ->
            Left
              (UnsupportedEquation leftExpression rightExpression)

atomicMorphismId :: ArrowExpr -> Maybe FinMorphismId
atomicMorphismId expression =
  case expression of
    ExprIdentity objectId _ ->
      Just (FinIdentityId objectId)
    ExprGen generatorId _ _ _ ->
      Just (FinGeneratorMorphismId generatorId)
    ExprComp _ _ ->
      Nothing

expressionEndpoints ::
  ArrowExpr ->
  Either FinCatBuildError (FinObjectId, FinObjectId)
expressionEndpoints expression =
  case expression of
    ExprIdentity objectId _ ->
      Right (objectId, objectId)
    ExprGen _ sourceId targetId _ ->
      Right (sourceId, targetId)
    ExprComp leftExpression rightExpression -> do
      (leftSource, leftTarget) <-
        expressionEndpoints leftExpression
      (rightSource, rightTarget) <-
        expressionEndpoints rightExpression
      if rightTarget == leftSource
        then Right (rightSource, leftTarget)
        else
          Left
            ( NonComposablePath
                leftExpression
                rightExpression
                rightTarget
                leftSource
            )

solveEquations ::
  Map CompositionKey CompositionClaim ->
  Map CompositionKey (Seq WaitingEquation) ->
  Seq OrientedEquation ->
  Either FinCatBuildError (Map CompositionKey CompositionClaim)
solveEquations !claims !waiting !ready =
  case Seq.viewl ready of
    EmptyL ->
      case firstWaitingExpression waiting of
        Nothing ->
          Right claims
        Just unresolvedExpression ->
          Left (UnresolvedComposite unresolvedExpression)
    equation :< remaining -> do
      attempt <- attemptEquation claims equation
      case attempt of
        EquationBlocked
          (BlockedExpression dependencyKey unresolvedExpression) ->
            solveEquations
              claims
              ( enqueueWaitingEquation
                  dependencyKey
                  (WaitingEquation unresolvedExpression equation)
                  waiting
              )
              remaining
        EquationSatisfied ->
          solveEquations claims waiting remaining
        EquationClaims compositionKey proposedClaim -> do
          (claimWasInserted, nextClaims) <-
            insertCompositionClaim
              compositionKey
              proposedClaim
              claims
          if claimWasInserted
            then
              let awakened =
                    Map.lookup compositionKey waiting
                  nextWaiting =
                    Map.delete compositionKey waiting
                  nextReady =
                    remaining
                      Seq.>< maybe
                        Seq.empty
                        waitingEquations
                        awakened
               in solveEquations
                    nextClaims
                    nextWaiting
                    nextReady
            else
              solveEquations nextClaims waiting remaining

waitingEquations ::
  Seq WaitingEquation ->
  Seq OrientedEquation
waitingEquations =
  fmap
    (\(WaitingEquation _ equation) -> equation)

firstWaitingExpression ::
  Map CompositionKey (Seq WaitingEquation) ->
  Maybe ArrowExpr
firstWaitingExpression waiting = do
  (_, equations) <- Map.lookupMin waiting
  case Seq.viewl equations of
    EmptyL ->
      Nothing
    WaitingEquation unresolvedExpression _ :< _ ->
      Just unresolvedExpression

enqueueWaitingEquation ::
  CompositionKey ->
  WaitingEquation ->
  Map CompositionKey (Seq WaitingEquation) ->
  Map CompositionKey (Seq WaitingEquation)
enqueueWaitingEquation dependencyKey equation =
  Map.alter
    ( \maybeEquations ->
        Just
          ( maybe
              (Seq.singleton equation)
              (|> equation)
              maybeEquations
          )
    )
    dependencyKey

attemptEquation ::
  Map CompositionKey CompositionClaim ->
  OrientedEquation ->
  Either FinCatBuildError EquationAttempt
attemptEquation
  claims
  (OrientedEquation compositeExpression resultExpression resultId) =
    case compositeExpression of
      ExprComp leftExpression rightExpression ->
        case resolveArrowExpression claims leftExpression of
          Left blockedExpression ->
            Right (EquationBlocked blockedExpression)
          Right leftId ->
            case resolveArrowExpression claims rightExpression of
              Left blockedExpression ->
                Right (EquationBlocked blockedExpression)
              Right rightId ->
                case
                  identityComposite
                    leftExpression
                    leftId
                    rightExpression
                    rightId
                  of
                    Just (actualId, expectedExpression)
                      | actualId == resultId ->
                          Right EquationSatisfied
                      | otherwise ->
                          Left
                            ( IdentityEquationMismatch
                                compositeExpression
                                resultExpression
                                expectedExpression
                            )
                    Nothing ->
                      Right
                        ( EquationClaims
                            (leftId, rightId)
                            ( CompositionClaim
                                compositeExpression
                                resultExpression
                                resultId
                            )
                        )
      _ ->
        Left
          (UnsupportedEquation compositeExpression resultExpression)

resolveArrowExpression ::
  Map CompositionKey CompositionClaim ->
  ArrowExpr ->
  Either BlockedExpression FinMorphismId
resolveArrowExpression claims expression =
  case expression of
    ExprIdentity objectId _ ->
      Right (FinIdentityId objectId)
    ExprGen generatorId _ _ _ ->
      Right (FinGeneratorMorphismId generatorId)
    ExprComp leftExpression rightExpression -> do
      leftId <-
        resolveArrowExpression claims leftExpression
      rightId <-
        resolveArrowExpression claims rightExpression
      case
        identityComposite
          leftExpression
          leftId
          rightExpression
          rightId
        of
          Just (composedId, _) ->
            Right composedId
          Nothing ->
            let compositionKey =
                  (leftId, rightId)
             in case Map.lookup compositionKey claims of
                  Nothing ->
                    Left
                      ( BlockedExpression
                          compositionKey
                          expression
                      )
                  Just claim ->
                    Right (compositionClaimResultId claim)

identityComposite ::
  ArrowExpr ->
  FinMorphismId ->
  ArrowExpr ->
  FinMorphismId ->
  Maybe (FinMorphismId, ArrowExpr)
identityComposite leftExpression leftId rightExpression rightId =
  case leftId of
    FinIdentityId _ ->
      Just (rightId, rightExpression)
    FinGeneratorMorphismId _ ->
      case rightId of
        FinIdentityId _ ->
          Just (leftId, leftExpression)
        FinGeneratorMorphismId _ ->
          Nothing

insertCompositionClaim ::
  CompositionKey ->
  CompositionClaim ->
  Map CompositionKey CompositionClaim ->
  Either
    FinCatBuildError
    (Bool, Map CompositionKey CompositionClaim)
insertCompositionClaim compositionKey proposedClaim claims =
  case Map.lookup compositionKey claims of
    Nothing ->
      Right
        ( True,
          Map.insert compositionKey proposedClaim claims
        )
    Just existingClaim
      | compositionClaimResultId existingClaim
          == compositionClaimResultId proposedClaim ->
          Right (False, claims)
      | otherwise ->
          Left
            ( ConflictingComposition
                (compositionClaimComposite existingClaim)
                (compositionClaimResultExpression existingClaim)
                (compositionClaimComposite proposedClaim)
                (compositionClaimResultExpression proposedClaim)
            )

compositionClaimComposite :: CompositionClaim -> ArrowExpr
compositionClaimComposite
  (CompositionClaim compositeExpression _ _) =
    compositeExpression

compositionClaimResultExpression ::
  CompositionClaim ->
  ArrowExpr
compositionClaimResultExpression
  (CompositionClaim _ resultExpression _) =
    resultExpression

compositionClaimResultId ::
  CompositionClaim ->
  FinMorphismId
compositionClaimResultId
  (CompositionClaim _ _ resultId) =
    resultId

firstDanglingEquationObject ::
  Int ->
  [(ArrowExpr, ArrowExpr)] ->
  Maybe FinObjectId
firstDanglingEquationObject objectCount =
  foldr
    ( \(leftExpression, rightExpression) nextDanglingObject ->
        firstDanglingExpressionObject objectCount leftExpression
          <|> firstDanglingExpressionObject objectCount rightExpression
          <|> nextDanglingObject
    )
    Nothing

firstDanglingExpressionObject ::
  Int ->
  ArrowExpr ->
  Maybe FinObjectId
firstDanglingExpressionObject objectCount expression =
  case expression of
    ExprIdentity objectId _ ->
      danglingObject objectCount objectId
    ExprGen _ sourceId targetId _ ->
      danglingObject objectCount sourceId
        <|> danglingObject objectCount targetId
    ExprComp leftExpression rightExpression ->
      firstDanglingExpressionObject objectCount leftExpression
        <|> firstDanglingExpressionObject objectCount rightExpression

danglingObject :: Int -> FinObjectId -> Maybe FinObjectId
danglingObject objectCount objectId@(FinObjectId objectIndex) =
  if objectIndex < 0 || objectIndex >= objectCount
    then Just objectId
    else Nothing

importRowsFromEdges ::
  Int ->
  [(FinObjectId, FinObjectId)] ->
  Vector Integer
importRowsFromEdges objectCount edges =
  let rowsBySource =
        foldl' insertEdge IntMap.empty edges
   in Vector.generate
        objectCount
        (\sourceIndex ->
           IntMap.findWithDefault 0 sourceIndex rowsBySource
        )
  where
    insertEdge ::
      IntMap Integer ->
      (FinObjectId, FinObjectId) ->
      IntMap Integer
    insertEdge
      rows
      (FinObjectId sourceIndex, FinObjectId targetIndex)
        | sourceIndex >= 0,
          sourceIndex < objectCount,
          targetIndex >= 0,
          targetIndex < objectCount =
            IntMap.insertWith
              (.|.)
              sourceIndex
              (bit targetIndex)
              rows
        | otherwise =
            rows

objectIdsFromComponents ::
  [NonEmpty Int] ->
  [FinObjectId]
objectIdsFromComponents components =
  fmap FinObjectId (Set.toAscList (foldr insertComponent Set.empty components))
  where
    insertComponent :: NonEmpty Int -> Set Int -> Set Int
    insertComponent component objectIds =
      foldr Set.insert objectIds component

danglingEndpoints ::
  Int ->
  [FinObjectId] ->
  [FinObjectId]
danglingEndpoints objectCount =
  foldr
    ( \objectId rest ->
        case danglingObject objectCount objectId of
          Nothing ->
            rest
          Just badObject ->
            badObject : rest
    )
    []