packages feed

moonlight-algebra-0.1.0.0: src-finite-lattice/Moonlight/FiniteLattice/Presentation.hs

{-# LANGUAGE DerivingStrategies #-}

-- | A monadic, name-binding builder for finite lattices that reads as mathematics
-- and compiles to a runtime-validated 'ContextLattice'.
--
-- You declare elements and an order (@a \`below\` b@ for @a ≤ b@); the runner infers
-- the universe and the (unique) top and bottom, then hands the order to
-- 'compileContextLattice', which transitively closes it, /derives/ join and meet, and
-- proves lattice-hood. The builder adds only the ergonomic frontend — every lattice
-- obligation (missing/ambiguous join or meet, antisymmetry, top-greatest,
-- bottom-least) stays with 'compileContextLattice' and surfaces as 'InvalidLattice'.
module Moonlight.FiniteLattice.Presentation
  ( LatticeBuilder,
    ElemRef,
    LatticeBuildError (..),
    LatticeBuilderPatternFailure (..),
    element,
    elements,
    below,
    belowAll,
    latticeOf,
    boundedLatticeOf,
  )
where

import Data.Bifunctor (first)
import Data.Coerce (coerce)
import Data.Kind (Type)
import Data.List qualified as List
import Data.Set (Set)
import qualified Data.Set as Set
import Moonlight.FiniteLattice.Core
  ( ContextLattice,
    ContextLatticeCompileError,
    compileContextLattice,
    contextOrderDecl,
  )

-- | An opaque reference to a declared element, carrying its value. Obtained from
-- 'element'; never constructed directly, so an order edge can only mention elements
-- the presentation declared.
type ElemRef :: Type -> Type
newtype ElemRef c = ElemRef c
  deriving stock (Eq, Show)

type LatticeBuildError :: Type -> Type
data LatticeBuildError c
  = DuplicateElement c
  | EmptyLattice
  | NoTop
  | AmbiguousTop [c]
  | NoBottom
  | AmbiguousBottom [c]
  | BuilderPatternFailure LatticeBuilderPatternFailure
  | InvalidLattice (ContextLatticeCompileError c)
  deriving stock (Eq, Show)

-- | Refutable do-pattern failure emitted by the 'MonadFail' instance.
type LatticeBuilderPatternFailure :: Type
newtype LatticeBuilderPatternFailure = LatticeBuilderPatternFailure
  { lbpfMessage :: String
  }
  deriving stock (Eq, Show)

type DeclarationList :: Type -> Type
type DeclarationList value = [value] -> [value]

type BuilderState :: Type -> Type
data BuilderState c = BuilderState
  { bsElements :: !(DeclarationList c),
    bsElementCount :: !Int,
    bsElementSet :: !(Set c),
    bsEdges :: !(DeclarationList (c, c)),
    bsStrictSources :: !(Set c),
    bsStrictTargets :: !(Set c),
    bsTrackBounds :: !Bool,
    bsErrors :: !(DeclarationList (LatticeBuildError c))
  }


emptyDeclarationList :: DeclarationList value
emptyDeclarationList =
  id

appendDeclaredValues :: DeclarationList value -> [value] -> DeclarationList value
appendDeclaredValues declaredValuesToDate newValues =
  declaredValuesToDate . (newValues <>)

declaredValues :: DeclarationList value -> [value]
declaredValues values =
  values []

initialState :: BuilderState c
initialState =
  BuilderState
    { bsElements = emptyDeclarationList,
      bsElementCount = 0,
      bsElementSet = Set.empty,
      bsEdges = emptyDeclarationList,
      bsStrictSources = Set.empty,
      bsStrictTargets = Set.empty,
      bsTrackBounds = True,
      bsErrors = emptyDeclarationList
    }

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

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

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

instance Monad (LatticeBuilder c) where
  LatticeBuilder run >>= k =
    LatticeBuilder
      ( \state ->
          case run state of
            (Nothing, state') -> (Nothing, state')
            (Just value, state') -> unLatticeBuilder (k value) state'
      )

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

recordError :: LatticeBuildError c -> BuilderState c -> BuilderState c
recordError buildError state =
  state {bsErrors = appendDeclaredValues (bsErrors state) [buildError]}

element :: Ord c => c -> LatticeBuilder c (ElemRef c)
element value =
  LatticeBuilder
    ( \state ->
        if Set.member value (bsElementSet state)
          then (Just (ElemRef value), recordError (DuplicateElement value) state)
          else
            ( Just (ElemRef value),
              recordNewElement value state
            )
    )

elements :: Ord c => [c] -> LatticeBuilder c [ElemRef c]
elements values =
  LatticeBuilder
    ( \state ->
        let valueCount = length values
            valueSet = elementValueSet values
         in if Set.size valueSet == valueCount && elementBatchDisjoint state valueSet
              then
                ( Just (coerce values),
                  recordNewElements values valueCount valueSet state
                )
              else
                let batch = classifyElementBatch (bsElementSet state) values
                    newValues = reverse (ebNewValues batch)
                 in ( Just (coerce values),
                      state
                        { bsElements = appendElementDeclarations state newValues,
                          bsElementCount = bsElementCount state + ebNewCount batch,
                          bsElementSet = ebSeen batch,
                          bsErrors = appendDeclaredValues (bsErrors state) (fmap DuplicateElement (reverse (ebDuplicateValues batch)))
                        }
                    )
    )

-- | Declare that the first element is below the second in the order (@a ≤ b@).
below :: Ord c => ElemRef c -> ElemRef c -> LatticeBuilder c ()
below (ElemRef lowerValue) (ElemRef upperValue) =
  LatticeBuilder
    ( \state ->
        ( Just (),
          state
            { bsEdges = appendDeclaredValues (bsEdges state) [(lowerValue, upperValue)],
              bsStrictSources =
                if not (bsTrackBounds state) || lowerValue == upperValue
                  then bsStrictSources state
                  else Set.insert lowerValue (bsStrictSources state),
              bsStrictTargets =
                if not (bsTrackBounds state) || lowerValue == upperValue
                  then bsStrictTargets state
                  else Set.insert upperValue (bsStrictTargets state)
            }
        )
    )

belowAll :: Ord c => [(ElemRef c, ElemRef c)] -> LatticeBuilder c ()
belowAll edgeRefs =
  LatticeBuilder
    ( \state ->
        let edgeValues = coerce edgeRefs
            stateWithEdges = state {bsEdges = appendDeclaredValues (bsEdges state) edgeValues}
         in ( Just (),
              if bsTrackBounds state
                then
                  stateWithEdges
                    { bsStrictSources = Set.union (strictSources edgeValues) (bsStrictSources state),
                      bsStrictTargets = Set.union (strictTargets edgeValues) (bsStrictTargets state)
                    }
                else stateWithEdges
            )
    )

-- | Run a presentation, inferring the universe and the unique top and bottom, then
-- compiling and proving lattice-hood, or returning the first fault.
latticeOf :: Ord c => LatticeBuilder c a -> Either (LatticeBuildError c) (ContextLattice c)
latticeOf builder =
  let (_, state) = unLatticeBuilder builder initialState
   in case declaredValues (bsErrors state) of
        (firstError : _) -> Left firstError
        [] -> compilePresentation state

-- | Run a presentation with declared top and bottom, skipping top/bottom
-- inference while preserving the same compile-time lattice proof.
boundedLatticeOf :: Ord c => c -> c -> LatticeBuilder c a -> Either (LatticeBuildError c) (ContextLattice c)
boundedLatticeOf topValue bottomValue builder =
  let (_, state) = unLatticeBuilder builder (initialState {bsTrackBounds = False})
   in case declaredValues (bsErrors state) of
        (firstError : _) -> Left firstError
        [] -> compileBoundedPresentation topValue bottomValue state

compilePresentation :: Ord c => BuilderState c -> Either (LatticeBuildError c) (ContextLattice c)
compilePresentation state =
  case declaredValues (bsElements state) of
    [] -> Left EmptyLattice
    universeList ->
      do
        let edgeList = declaredValues (bsEdges state)
        topValue <- inferTop universeList (bsStrictSources state)
        bottomValue <- inferBottom universeList (bsStrictTargets state)
        first
          InvalidLattice
          (compileContextLattice (bsElementSet state) (contextOrderDecl topValue bottomValue edgeList))

compileBoundedPresentation :: Ord c => c -> c -> BuilderState c -> Either (LatticeBuildError c) (ContextLattice c)
compileBoundedPresentation topValue bottomValue state =
  if bsElementCount state == 0
    then Left EmptyLattice
    else
      first
        InvalidLattice
        (compileContextLattice (bsElementSet state) (contextOrderDecl topValue bottomValue (declaredValues (bsEdges state))))

-- | The unique maximal element (one with no strictly-outgoing edge). In a finite
-- poset a unique maximal element is the greatest, so this is the top.
inferTop :: Ord c => [c] -> Set c -> Either (LatticeBuildError c) c
inferTop universeList strictSourceSet =
  case filter (`Set.notMember` strictSourceSet) universeList of
    [topValue] -> Right topValue
    [] -> Left NoTop
    candidates -> Left (AmbiguousTop candidates)

-- | The unique minimal element (one with no strictly-incoming edge).
inferBottom :: Ord c => [c] -> Set c -> Either (LatticeBuildError c) c
inferBottom universeList strictTargetSet =
  case filter (`Set.notMember` strictTargetSet) universeList of
    [bottomValue] -> Right bottomValue
    [] -> Left NoBottom
    candidates -> Left (AmbiguousBottom candidates)

type ElementBatch :: Type -> Type
data ElementBatch c = ElementBatch
  { ebSeen :: !(Set c),
    ebNewCount :: !Int,
    ebNewValues :: ![c],
    ebDuplicateValues :: ![c]
  }

classifyElementBatch :: Ord c => Set c -> [c] -> ElementBatch c
classifyElementBatch initialSeen =
  List.foldl' classifyElement (ElementBatch initialSeen 0 [] [])

classifyElement :: Ord c => ElementBatch c -> c -> ElementBatch c
classifyElement batch value
  | Set.member value (ebSeen batch) =
      batch {ebDuplicateValues = value : ebDuplicateValues batch}
  | otherwise =
      batch
        { ebSeen = Set.insert value (ebSeen batch),
          ebNewCount = ebNewCount batch + 1,
          ebNewValues = value : ebNewValues batch
        }

recordNewElement :: Ord c => c -> BuilderState c -> BuilderState c
recordNewElement value state =
  state
    { bsElements =
        if bsTrackBounds state
          then appendDeclaredValues (bsElements state) [value]
          else bsElements state,
      bsElementCount = bsElementCount state + 1,
      bsElementSet = Set.insert value (bsElementSet state)
    }

recordNewElements :: Ord c => [c] -> Int -> Set c -> BuilderState c -> BuilderState c
recordNewElements values valueCount valueSet state =
  state
    { bsElements = appendElementDeclarations state values,
      bsElementCount = bsElementCount state + valueCount,
      bsElementSet =
        if Set.null (bsElementSet state)
          then valueSet
          else Set.union valueSet (bsElementSet state)
    }

elementBatchDisjoint :: Ord c => BuilderState c -> Set c -> Bool
elementBatchDisjoint state valueSet =
  Set.null (bsElementSet state) || Set.null (Set.intersection (bsElementSet state) valueSet)

appendElementDeclarations :: BuilderState c -> [c] -> DeclarationList c
appendElementDeclarations state values =
  if bsTrackBounds state
    then appendDeclaredValues (bsElements state) values
    else bsElements state

elementValueSet :: Ord c => [c] -> Set c
elementValueSet values
  | strictlyAscending values = Set.fromDistinctAscList values
  | otherwise = Set.fromList values

strictlyAscending :: Ord c => [c] -> Bool
strictlyAscending values =
  and [leftValue < rightValue | (leftValue, rightValue) <- zip values (drop 1 values)]

strictSources :: Ord c => [(c, c)] -> Set c
strictSources edgeValues =
  Set.fromList [lowerValue | (lowerValue, upperValue) <- edgeValues, lowerValue /= upperValue]

strictTargets :: Ord c => [(c, c)] -> Set c
strictTargets edgeValues =
  Set.fromList [upperValue | (lowerValue, upperValue) <- edgeValues, lowerValue /= upperValue]