packages feed

distributors-0.4.0.0: src/Control/Lens/Grammar/Kleene.hs

{- |
Module      : Control.Lens.Grammar.Kleene
Description : Kleene star algebras & regular expressions
Copyright   : (C) 2026 - Eitan Chatav
License     : BSD-style (see the file LICENSE)
Maintainer  : Eitan Chatav <eitan.chatav@gmail.com>
Stability   : provisional
Portability : non-portable

Regular expressions form a Kleene star algebra. See Kleene,
[Representation of Events in Nerve Nets and Finite Automata]
(https://www.rand.org/pubs/research_memoranda/RM704.html)
-}

module Control.Lens.Grammar.Kleene
  ( -- * KleeneStarAlgebra
    KleeneStarAlgebra (..)
  , orK, anyK
    -- * TokenAlgebra
  , TokenAlgebra (..)
    -- * RegEx & TokenClass
  , RegEx (..)
  , TokenClass (..)
  , RegExam (..)
  , CategoryTest (..)
  ) where

import Control.Applicative
import Control.Lens.Grammar.Boole
import Control.Lens.Grammar.Symbol
import Control.Lens.Grammar.Token
import Data.Foldable
import Data.MemoTrie
import Data.Monoid
import Data.Profunctor
import Data.Profunctor.Distributor
import Data.Set (Set)
import qualified Data.Set as Set
import GHC.Generics

{- | A `KleeneStarAlgebra` is a ring
with a generally non-commutative multiplication,
the `Monoid` concatenation operator `<>` with identity `mempty`;
and an idempotent addition, the alternation operator `>|<`
with identity `zeroK`.

It has three unary operators `optK`, `plusK` and the eponymous `starK`.

prop> starK x = optK (plusK x)
prop> plusK x = x <> starK x
prop> optK x = mempty >|< x

The following invariants should hold.

prop> x >|< x = x
prop> zeroK >|< x = x = x >|< zeroK
prop> mempty >|< x = optK x = x >|< mempty
prop> zeroK <> x = zeroK = x <> zeroK
prop> mempty <> x = x = x <> mempty

-}
class Monoid k => KleeneStarAlgebra k where
  starK, plusK, optK :: k -> k
  starK x = optK (plusK x)
  plusK x = x <> starK x
  optK x = mempty >|< x
  infixl 3 >|<
  (>|<) :: k -> k -> k
  zeroK :: k
  default (>|<) :: (k ~ f a, Alternative f) => k -> k -> k
  default zeroK :: (k ~ f a, Alternative f) => k
  (>|<) = (<|>)
  zeroK = empty

-- | cumulative alternation
orK :: (Foldable f, KleeneStarAlgebra k) => f k -> k
orK = foldl' (>|<) zeroK

-- | existential
anyK :: (Foldable f, KleeneStarAlgebra k) => (a -> k) -> f a -> k
anyK f = foldl' (\b a -> b >|< f a) zeroK

{- | The `RegEx`pression type forms the prototypical `KleeneStarAlgebra`.
It is also a `TokenAlgebra`, such that the following invariants hold.

prop> zeroK = tokenClass falseB
prop> tokenClass x >|< tokenClass y = tokenClass (x >||< y)

-}
data RegEx token
  = SeqEmpty
  | Sequence (RegEx token) (RegEx token)
  | NonTerminal String
  | KleeneStar (RegEx token)
  | KleeneOpt (RegEx token)
  | KleenePlus (RegEx token)
  | RegExam (RegExam token (RegEx token))

{- | A component of both `RegEx`pressions and `TokenClass`es,
so that the latter can be embedded in the former with `tokenClass`.
-}
data RegExam token alg
  = OneOf (Set token)
  | NotOneOf (Set token) (CategoryTest token)
  | Alternate alg alg

failExam :: RegExam token alg
failExam = OneOf Set.empty

passExam :: RegExam token alg
passExam = NotOneOf Set.empty (AndNotAsIn Set.empty)

isFailExam :: RegExam token alg -> Bool
isFailExam (OneOf xs) = Set.null xs
isFailExam _ = False

isPassExam :: RegExam token alg -> Bool
isPassExam (NotOneOf xs (AndNotAsIn ys)) = Set.null xs && Set.null ys
isPassExam _ = False

{- | `CategoryTest`s for `Categorized` tokens.-}
data CategoryTest token
  = AndAsIn (Categorize token)
  | AndNotAsIn (Set (Categorize token))

{- | `TokenClass` forms a `Tokenized` `BooleanAlgebra`,
such that the following invariants hold.

prop> trueB = anyToken
prop> trueB = notOneOf []
prop> falseB = oneOf []
prop> notB . oneOf = notOneOf
prop> notB . notOneOf = oneOf
prop> notB . asIn = notAsIn
prop> notB . notAsIn = asIn

-}
newtype TokenClass token = TokenClass (RegExam token (TokenClass token))

{- | `TokenAlgebra` extends `Tokenized` methods to support
`BooleanAlgebra` operations within a `tokenClass`.
When a `TokenAlgebra` is an `Alternative`,
then `tokenClass` is expected to act homomorphically on disjunction.

prop> empty = tokenClass falseB
prop> tokenClass x <|> tokenClass y = tokenClass (x >||< y)

-}
class Tokenized token p => TokenAlgebra token p where
  tokenClass :: TokenClass token -> p
  default tokenClass
    :: (p ~ q token token, Alternator q, Cochoice q)
    => TokenClass token -> p
  tokenClass (TokenClass exam) = case exam of
    OneOf chars -> oneOf chars
    NotOneOf chars (AndAsIn cat) ->
      satisfy (notOneOf chars >&&< asIn cat)
    NotOneOf chars (AndNotAsIn cats) ->
      satisfy (notOneOf chars >&&< allB notAsIn cats)
    Alternate exam1 exam2 -> tokenClass exam1 <|> tokenClass exam2

--instances
instance (Alternative f, Monoid k) => KleeneStarAlgebra (Ap f k)
deriving stock instance Generic (RegEx token)
deriving stock instance Generic (RegExam token alg)
deriving stock instance Generic1 (RegExam token)
deriving stock instance Generic (TokenClass token)
deriving stock instance Generic (CategoryTest token)
deriving stock instance Categorized token => Eq (RegEx token)
deriving stock instance Categorized token => Ord (RegEx token)
deriving stock instance
  (Categorized token, Read token, Read (Categorize token))
    => Read (RegEx token)
deriving stock instance
  (Categorized token, Show token, Show (Categorize token))
    => Show (RegEx token)
deriving stock instance
  (Categorized token, Read token, Read (Categorize token))
    => Read (TokenClass token)
deriving stock instance
  (Categorized token, Show token, Show (Categorize token))
    => Show (TokenClass token)
deriving newtype instance Categorized token => Eq (TokenClass token)
deriving newtype instance Categorized token => Ord (TokenClass token)
deriving newtype instance Categorized token => Tokenized token (TokenClass token)
deriving newtype instance Categorized token => BooleanAlgebra (TokenClass token)
instance Categorized token
  => TokenAlgebra token (TokenClass token) where
  tokenClass = id
instance Categorized token
  => TokenAlgebra token (RegExam token (TokenClass token)) where
  tokenClass (TokenClass exam) = exam
instance Categorized token => TerminalSymbol token (RegEx token) where
  terminal = foldl (\acc t -> acc <> token t) mempty
instance NonTerminalSymbol (RegEx token) where
  nonTerminal = NonTerminal
instance Categorized token => Tokenized token (RegEx token) where
  anyToken = RegExam passExam
  token a = RegExam (OneOf (Set.singleton a))
  oneOf as = RegExam (OneOf (Set.fromList (toList as)))
  notOneOf as =
    RegExam (NotOneOf (Set.fromList (toList as)) (AndNotAsIn Set.empty))
  asIn cat = RegExam (NotOneOf Set.empty (AndAsIn cat))
  notAsIn cat = RegExam (NotOneOf Set.empty (AndNotAsIn (Set.singleton cat)))
instance Categorized token => TokenAlgebra token (token -> Bool) where
  tokenClass (TokenClass exam) x = case exam of
    OneOf xs -> Set.member x xs
    NotOneOf xs (AndAsIn y) ->
      Set.notMember x xs && categorize x == y
    NotOneOf xs (AndNotAsIn ys) ->
      Set.notMember x xs && Set.notMember (categorize x) ys
    Alternate exam1 exam2 ->
      tokenClass exam1 x || tokenClass exam2 x
instance Categorized token => TokenAlgebra token (RegEx token) where
  tokenClass (TokenClass exam) = case exam of
    OneOf as -> RegExam (OneOf as)
    NotOneOf as catTest -> RegExam (NotOneOf as catTest)
    Alternate exam1 exam2 ->
      RegExam (Alternate (tokenClass exam1) (tokenClass exam2))
instance Categorized token => Monoid (RegEx token) where
  mempty = SeqEmpty
instance Categorized token => Semigroup (RegEx token) where
  SeqEmpty <> rex = rex
  rex <> SeqEmpty = rex
  RegExam exam <> _ | isFailExam exam = zeroK
  _ <> RegExam exam | isFailExam exam = zeroK
  KleeneStar rex0 <> rex1 | rex0 == rex1 = plusK rex0
  rex0 <> KleeneStar rex1 | rex0 == rex1 = plusK rex1
  rex0 <> rex1 = Sequence rex0 rex1
instance Categorized token => KleeneStarAlgebra (RegEx token) where
  zeroK = RegExam failExam
  optK (RegExam exam) | isFailExam exam = mempty
  optK SeqEmpty = mempty
  optK (KleenePlus rex) = starK rex
  optK rex = KleeneOpt rex
  starK (RegExam exam) | isFailExam exam = mempty
  starK SeqEmpty = mempty
  starK rex = KleeneStar rex
  plusK (RegExam exam) | isFailExam exam = zeroK
  plusK SeqEmpty = mempty
  plusK rex = KleenePlus rex
  rex0 >|< rex1 | rex0 == rex1 = rex0
  KleenePlus rex >|< SeqEmpty = starK rex
  SeqEmpty >|< KleenePlus rex = starK rex
  rex >|< SeqEmpty = optK rex
  SeqEmpty >|< rex = optK rex
  rex >|< RegExam exam | isFailExam exam = rex
  RegExam exam >|< rex | isFailExam exam = rex
  rex0 >|< rex1 | Just tokenOr <- maybeOr = tokenClass tokenOr
    where
      toTokenClass (RegExam exam) =
        TokenClass <$> traverse toTokenClass exam
      toTokenClass _ = Nothing
      maybeOr = (>||<) <$> toTokenClass rex0 <*> toTokenClass rex1
  rex0 >|< rex1 = RegExam (Alternate rex0 rex1)
instance Categorized token => Tokenized token (RegExam token alg) where
  anyToken = passExam
  token a = OneOf (Set.singleton a)
  oneOf as | null as = failExam
  oneOf as = OneOf (Set.fromList (toList as))
  notOneOf as | null as = passExam
  notOneOf as =
    NotOneOf (Set.fromList (toList as)) (AndNotAsIn Set.empty)
  asIn cat = NotOneOf Set.empty (AndAsIn cat)
  notAsIn cat = NotOneOf Set.empty (AndNotAsIn (Set.singleton cat))
instance Categorized token
  => BooleanAlgebra (RegExam token (TokenClass token)) where
  falseB = failExam
  trueB = passExam
  notB exam | isFailExam exam = passExam
  notB exam | isPassExam exam = failExam
  notB (Alternate (TokenClass x) (TokenClass y)) = notB x >&&< notB y
  notB (OneOf xs) = notOneOf xs
  notB (NotOneOf xs (AndAsIn y)) = oneOf xs >||< notAsIn y
  notB (NotOneOf xs (AndNotAsIn ys)) = oneOf xs >||< anyB asIn ys
  x >&&< y | x == y = x
  _ >&&< exam | isFailExam exam = failExam
  exam >&&< _ | isFailExam exam = failExam
  x >&&< exam | isPassExam exam = x
  exam >&&< z | isPassExam exam = z
  x >&&< Alternate (TokenClass y) (TokenClass z) = (x >&&< y) >||< (x >&&< z)
  Alternate (TokenClass x) (TokenClass y) >&&< z = (x >&&< z) >||< (y >&&< z)
  OneOf xs >&&< OneOf ys = OneOf (Set.intersection xs ys)
  OneOf xs >&&< NotOneOf ys (AndAsIn z) = OneOf
    (Set.filter (\x -> categorize x == z) (Set.difference xs ys))
  NotOneOf xs (AndAsIn y) >&&< OneOf zs = OneOf
    (Set.filter (\z -> categorize z == y) (Set.difference zs xs))
  OneOf xs >&&< NotOneOf ys (AndNotAsIn zs) = OneOf
    (Set.filter (\x -> categorize x `notElem` zs) (Set.difference xs ys))
  NotOneOf xs (AndNotAsIn ys) >&&< OneOf zs = OneOf
    (Set.filter (\z -> categorize z `notElem` ys) (Set.difference zs xs))
  NotOneOf xs (AndAsIn y) >&&< NotOneOf ws (AndAsIn z) =
    if y /= z then failExam else NotOneOf
      (Set.filter (\x -> categorize x == y) (Set.union xs ws)) (AndAsIn y)
  NotOneOf xs (AndAsIn y) >&&< NotOneOf ws (AndNotAsIn zs) =
    if y `elem` zs then failExam else NotOneOf
      (Set.filter (\x -> categorize x == y) (Set.union xs ws)) (AndAsIn y)
  NotOneOf xs (AndNotAsIn ys) >&&< NotOneOf ws (AndAsIn z) =
    if z `elem` ys then failExam else NotOneOf
      (Set.filter (\x -> categorize x == z) (Set.union xs ws)) (AndAsIn z)
  NotOneOf xs (AndNotAsIn ys) >&&< NotOneOf ws (AndNotAsIn zs) =
    let
      xws = Set.union xs ws
      yzs = Set.union ys zs
    in
      NotOneOf
        (Set.filter (\x -> categorize x `notElem` yzs) xws)
        (AndNotAsIn yzs)
  x >||< y | x == y = x
  x >||< exam | isFailExam exam = x
  exam >||< y | isFailExam exam = y
  _ >||< exam | isPassExam exam = passExam
  exam >||< _ | isPassExam exam = passExam
  OneOf xs >||< OneOf ys = oneOf (Set.union xs ys)
  x >||< y = Alternate (TokenClass x) (TokenClass y)
deriving stock instance
  (Categorized token, Read token, Read alg, Read (Categorize token))
    => Read (RegExam token alg)
deriving stock instance
  (Categorized token, Show token, Show alg, Show (Categorize token))
    => Show (RegExam token alg)
deriving stock instance Functor (RegExam token)
deriving stock instance Foldable (RegExam token)
deriving stock instance Traversable (RegExam token)
deriving stock instance (Categorized token, Eq alg) => Eq (RegExam token alg)
deriving stock instance (Categorized token, Ord alg) => Ord (RegExam token alg)
deriving stock instance Categorized token => Eq (CategoryTest token)
deriving stock instance Categorized token => Ord (CategoryTest token)
deriving stock instance
  (Categorized token, Read token, Read (Categorize token))
    => Read (CategoryTest token)
deriving stock instance
  (Categorized token, Show token, Show (Categorize token))
    => Show (CategoryTest token)
instance (Categorized token, HasTrie token)
  => HasTrie (RegEx token) where
    data (RegEx token :->: b) = RegExTrie
      { seqEmptyTrie :: b
      , nonTerminalTrie :: String :->: b
      , sequenceTrie :: (RegEx token, RegEx token) :->: b
      , alternateTrie :: (RegEx token, RegEx token) :->: b
      , kleeneStarTrie :: RegEx token :->: b
      , kleeneOptTrie :: RegEx token :->: b
      , kleenePlusTrie :: RegEx token :->: b
      , oneOfTrie :: [token] :->: b
      , notOneOfTrie :: ([token], Either Int [Int]) :->: b
      }
    trie f = RegExTrie
      { seqEmptyTrie = f mempty
      , nonTerminalTrie = trie (f . nonTerminal)
      , sequenceTrie = trie (f . uncurry (<>))
      , alternateTrie = trie (f . uncurry (>|<))
      , kleeneStarTrie = trie (f . starK)
      , kleeneOptTrie = trie (f . optK)
      , kleenePlusTrie = trie (f . plusK)
      , oneOfTrie = trie (f . oneOf)
      , notOneOfTrie = trie (f . testNotOneOf)
      }
    untrie rex = \case
      SeqEmpty -> seqEmptyTrie rex
      NonTerminal name -> untrie (nonTerminalTrie rex) name
      Sequence x1 x2 -> untrie (sequenceTrie rex) (x1,x2)
      KleeneStar x -> untrie (kleeneStarTrie rex) x
      KleenePlus x -> untrie (kleenePlusTrie rex) x
      KleeneOpt x -> untrie (kleeneOptTrie rex) x
      RegExam (OneOf chars) -> untrie (oneOfTrie rex) (Set.toList chars)
      RegExam (NotOneOf chars (AndAsIn cat)) ->
        untrie (notOneOfTrie rex) (Set.toList chars, Left (fromEnum cat))
      RegExam (NotOneOf chars (AndNotAsIn cats)) ->
        untrie (notOneOfTrie rex)
          (Set.toList chars, Right (Set.toList (Set.map fromEnum cats)))
      RegExam (Alternate x1 x2) -> untrie (alternateTrie rex) (x1,x2)
    enumerate rex = mconcat
      [ [(SeqEmpty, seqEmptyTrie rex)]
      , first' NonTerminal <$> enumerate (nonTerminalTrie rex)
      , first' (uncurry Sequence) <$> enumerate (sequenceTrie rex)
      , first' (RegExam . uncurry Alternate) <$> enumerate (alternateTrie rex)
      , first' KleeneStar <$> enumerate (kleeneStarTrie rex)
      , first' KleeneOpt <$> enumerate (kleeneOptTrie rex)
      , first' KleenePlus <$> enumerate (kleenePlusTrie rex)
      , first' (RegExam . OneOf . Set.fromList) <$> enumerate (oneOfTrie rex)
      , first' testNotOneOf <$> enumerate (notOneOfTrie rex)
      ]
testNotOneOf
  :: Categorized token
  => ([token], Either Int [Int]) -> RegEx token
testNotOneOf (chars, catTest) = RegExam $ NotOneOf
  (Set.fromList chars)
  (either (AndAsIn . toEnum) (AndNotAsIn . Set.map toEnum . Set.fromList) catTest)