crucible-debug-0.1.0: src/Lang/Crucible/Debug/Regex.hs
{-|
Copyright : (c) Galois, Inc. 2025
Maintainer : Langston Barrett <langston@galois.com>
-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
module Lang.Crucible.Debug.Regex
( type Regex
, type Empty
, type Lit
, type (:|)
, type Then
, type Star
, RegexRepr(..)
, AcceptsEmpty
, acceptsEmpty
, DerivativeResult(..)
, derivative
, Match(..)
, MatchError(..)
, TokenParser(..)
, match
, nextLits
, liftOrs
, Sugar(..)
, sugar
, prettySugar
) where
import Data.Bifunctor (first)
import Data.Foldable qualified as Foldable
import Data.Kind (Type)
import Data.Parameterized.BoolRepr (BoolRepr (..), (%||), (%&&))
import Data.Parameterized.Classes (KnownRepr(knownRepr))
import Data.Parameterized.Some (Some (Some))
import Data.Parameterized.TraversableFC qualified as TFC
import Data.Sequence qualified as Seq
import Data.Sequence (Seq)
import Data.Type.Bool (type (||), type (&&))
import Data.Type.Equality (TestEquality (testEquality), type (:~:)(Refl))
import Prettyprinter qualified as PP
-- | Type-level only
data Regex a
= TFail
| TEmpty
| TLit a
| TOr (Regex a) (Regex a)
| TThen (Regex a) (Regex a)
| TStar (Regex a)
type Empty = 'TEmpty
type Lit = 'TLit
type a :| b = 'TOr a b
type Then = 'TThen
type Star = 'TStar
-- | Value-level representative of 'Regex'
--
-- The order of the type parameters is a bit arbitrary... This order gives
-- 'KnownRepr' and 'TestEquality' instances but requires flipping the type
-- parameters to get 'TraversableF'.
type RegexRepr :: (k -> Type) -> Regex k -> Type
data RegexRepr f r where
Empty :: RegexRepr f 'TEmpty
Fail :: RegexRepr f 'TFail
Lit :: f t -> RegexRepr f ('TLit t)
Or :: RegexRepr f l -> RegexRepr f r -> RegexRepr f ('TOr l r)
Star :: RegexRepr f r -> RegexRepr f ('TStar r)
Then :: RegexRepr f l -> RegexRepr f r -> RegexRepr f ('TThen l r)
instance TestEquality f => TestEquality (RegexRepr f) where
testEquality rgx rgx' =
case (rgx, rgx') of
(Empty, Empty) -> Just Refl
(Empty, _) -> Nothing
(Fail, Fail) -> Just Refl
(Fail, _) -> Nothing
(Lit l, Lit l') ->
case testEquality l l' of
Just Refl -> Just Refl
Nothing -> Nothing
(Lit {}, _) -> Nothing
(Or l r, Or l' r') ->
case (testEquality l l', testEquality r r') of
(Just Refl, Just Refl) -> Just Refl
_ -> Nothing
(Or {}, _) -> Nothing
(Star r, Star r') ->
case testEquality r r' of
Just Refl -> Just Refl
Nothing -> Nothing
(Star {}, _) -> Nothing
(Then l r, Then l' r') ->
case (testEquality l l', testEquality r r') of
(Just Refl, Just Refl) -> Just Refl
_ -> Nothing
(Then {}, _) -> Nothing
instance KnownRepr (RegexRepr f) 'TEmpty where
knownRepr = Empty
instance KnownRepr (RegexRepr f) 'TFail where
knownRepr = Fail
instance KnownRepr f t => KnownRepr (RegexRepr f) ('TLit t) where
knownRepr = Lit knownRepr
instance
( KnownRepr (RegexRepr f) l
, KnownRepr (RegexRepr f) r
) => KnownRepr (RegexRepr f) ('TOr l r) where
knownRepr = Or knownRepr knownRepr
instance
( KnownRepr (RegexRepr f) l
, KnownRepr (RegexRepr f) r
) => KnownRepr (RegexRepr f) ('TThen l r) where
knownRepr = Then knownRepr knownRepr
instance KnownRepr (RegexRepr f) r => KnownRepr (RegexRepr f) ('TStar r) where
knownRepr = Star knownRepr
instance TFC.FunctorFC RegexRepr where
fmapFC f =
\case
Empty -> Empty
Fail -> Fail
Lit l -> Lit (f l)
Or l r -> Or (TFC.fmapFC f l) (TFC.fmapFC f r)
Star r -> Star (TFC.fmapFC f r)
Then l r -> Then (TFC.fmapFC f l) (TFC.fmapFC f r)
instance TFC.FoldableFC RegexRepr where
foldMapFC f =
\case
Empty -> mempty
Fail -> mempty
Lit l -> f l
Or l r -> TFC.foldMapFC f l <> TFC.foldMapFC f r
Star r -> TFC.foldMapFC f r
Then l r -> TFC.foldMapFC f l <> TFC.foldMapFC f r
instance TFC.TraversableFC RegexRepr where
traverseFC f =
\case
Empty -> pure Empty
Fail -> pure Fail
Lit l -> Lit <$> f l
Or l r -> Or <$> TFC.traverseFC f l <*> TFC.traverseFC f r
Star r -> Star <$> TFC.traverseFC f r
Then l r -> Then <$> TFC.traverseFC f l <*> TFC.traverseFC f r
type AcceptsEmpty :: Regex k -> Bool
type family AcceptsEmpty r where
AcceptsEmpty 'TFail = 'False
AcceptsEmpty 'TEmpty = 'True
AcceptsEmpty ('TLit _) = 'False
AcceptsEmpty ('TOr l r) = AcceptsEmpty l || AcceptsEmpty r
AcceptsEmpty ('TThen l r) = AcceptsEmpty l && AcceptsEmpty r
AcceptsEmpty ('TStar r) = 'True
acceptsEmpty :: RegexRepr f r -> BoolRepr (AcceptsEmpty r)
acceptsEmpty =
\case
Empty -> TrueRepr
Fail -> FalseRepr
Lit {} -> FalseRepr
Or l r -> acceptsEmpty l %|| acceptsEmpty r
Star _ -> TrueRepr
Then l r -> acceptsEmpty l %&& acceptsEmpty r
-- | Type-level version of 'nu'.
--
-- See Wikipedia on "Brzozowski derivative"
type Nu :: Regex k -> Regex k
type family Nu r where
Nu 'TFail = 'TFail
Nu 'TEmpty = 'TEmpty
Nu ('TLit _) = 'TFail
Nu ('TOr l r) = 'TOr (Nu l) (Nu r)
Nu ('TThen l r) = 'TThen (Nu l) (Nu r)
Nu ('TStar r) = 'TEmpty
-- | Auxiliary function for 'derivative'.
--
-- Value-level version of 'Nu'.
--
-- See Wikipedia on "Brzozowski derivative"
nu :: RegexRepr f r -> RegexRepr f (Nu r)
nu =
\case
Empty -> Empty
Fail -> Fail
Lit {} -> Fail
Or l r -> Or (nu l) (nu r)
Star _ -> Empty
Then l r -> Then (nu l) (nu r)
-- | The result of 'derivative'
data DerivativeResult f g
= DerivativeResult
{ -- | The remaining regex after matching that token
derivativeRegex :: Some (RegexRepr f)
-- | All the literals the token was matched at
, derivativeMatched :: Seq (Some g)
}
-- | See Wikipedia on "Brzozowski derivative"
derivative ::
(forall t. f t -> TokenParser tok e g t) ->
tok ->
RegexRepr f r ->
DerivativeResult f g
derivative getParser tok =
let noMatch r = DerivativeResult r Seq.empty in
let doFail = noMatch (Some Fail) in
\case
Empty -> doFail
Fail -> doFail
Lit f ->
case runTokenParser (getParser f) tok of
Left _ -> doFail
Right m -> DerivativeResult (Some Empty) (Seq.singleton (Some m))
Or l r ->
case (derivative getParser tok l, derivative getParser tok r) of
(DerivativeResult (Some Fail) _, r') -> r'
(l', DerivativeResult (Some Fail) _) -> l'
(DerivativeResult (Some l') ms, DerivativeResult (Some r') ms') ->
DerivativeResult (Some (Or l' r')) (ms <> ms')
Star f ->
case derivative getParser tok f of
DerivativeResult (Some f') ms ->
DerivativeResult (Some (Then f' (Star f))) ms
Then l r ->
case (derivative getParser tok l, derivative getParser tok r) of
(DerivativeResult (Some l') ms, DerivativeResult (Some r') ms') ->
DerivativeResult (Some (Or (Then l' r) (Then (nu l) r'))) (ms <> ms')
-- | The result of 'match', in case of success
type Match :: (k -> Type) -> Regex k -> Type
data Match k r where
MEmpty :: Match f 'TEmpty
MLit :: f t -> Match f ('TLit t)
MLeft :: Match f l -> Match f ('TOr l r)
MRight :: Match f r -> Match f ('TOr l r)
MThen :: Match f l -> Match f r -> Match f ('TThen l r)
MStar :: [Match f r] -> Match f ('TStar r)
-- | Failures that may arise during 'match'
data MatchError tok e
= EFail
| Eof
| EOr (MatchError tok e) (MatchError tok e)
| NotEmpty [tok]
| Token e
deriving Show
instance (PP.Pretty e, PP.Pretty tok) => PP.Pretty (MatchError tok e) where
pretty =
\case
EFail -> "This regular expression never matches"
Eof -> "Unexpected end of input"
EOr l r ->
PP.vcat
[ "Both branches failed to match:"
, "-" PP.<+> PP.align (PP.pretty l)
, "-" PP.<+> PP.align (PP.pretty r)
]
NotEmpty toks ->
"Expected end of input, but found:" PP.<+> PP.hsep (map PP.pretty toks)
Token e -> PP.pretty e
type TokenParser :: Type -> Type -> (k -> Type) -> k -> Type
newtype TokenParser tok e f t
= TokenParser { runTokenParser :: tok -> Either e (f t) }
-- | Match a regular expression against a token stream
match ::
-- | Regex
RegexRepr (TokenParser tok e g) r ->
-- | Token stream
[tok] ->
-- | Either a 'MatchError', or a 'Match' plus the unconsumed tokens
Either (MatchError tok e) (Match g r, [tok])
match rgx toks =
case (rgx, toks) of
(Fail, _) -> Left EFail
(Empty, []) -> Right (MEmpty, toks)
(Empty, _) -> Left (NotEmpty toks)
(Lit _, []) -> Left Eof
(Lit ft, t : ts) -> do
m <- first Token (runTokenParser ft t)
pure (MLit m, ts)
(Then l r, ts) -> do
(ml, ts') <- match l ts
(mr, ts'') <- match r ts'
pure (MThen ml mr, ts'')
(Or l r, ts) ->
case match l ts of
Right (m, ts') -> Right (MLeft m, ts')
Left ef ->
case match r ts of
Right (m, ts') -> pure (MRight m, ts')
Left eg -> Left (EOr ef eg)
(Star f, ts) ->
case match f ts of
Left _ -> Right (MStar [], ts)
Right (m, ts') ->
first (\(MStar ms) -> MStar (m : ms)) <$> match (Star f) ts'
-- | List the literals that could be matched next
nextLits :: RegexRepr f r -> Seq (Some f)
nextLits =
\case
Empty -> Seq.empty
Fail -> Seq.empty
Lit x -> Seq.singleton (Some x)
Or l r -> nextLits l <> nextLits r
Star r -> nextLits r
Then l r ->
case acceptsEmpty l of
TrueRepr -> nextLits l <> nextLits r
FalseRepr -> nextLits l
-- | Syntactic sugar for displaying 'RegexRepr's, especially for quantification
type Sugar :: (k -> Type) -> Regex k -> Type
data Sugar f r where
SEmpty :: Sugar f 'TEmpty
SFail :: Sugar f 'TFail
SLit :: f t -> Sugar f ('TLit t)
SOptL :: Sugar f l -> Sugar f ('TOr l Empty)
SOptR :: Sugar f r -> Sugar f ('TOr Empty r)
SOr :: Sugar f l -> Sugar f r -> Sugar f ('TOr l r)
SSomeL :: Sugar f l -> Sugar f ('TOr ('TStar r) r)
SSomeR :: Sugar f r -> Sugar f ('TOr r ('TStar r))
SStar :: Sugar f r -> Sugar f ('TStar r)
SThen :: Sugar f l -> Sugar f r -> Sugar f ('TThen l r)
sugar :: TestEquality f => RegexRepr f r -> Sugar f r
sugar =
\case
Empty -> SEmpty
Fail -> SFail
Lit l -> SLit l
Or l Empty -> SOptL (sugar l)
Or Empty r -> SOptR (sugar r)
Or (Star l) r | Just Refl <- testEquality l r -> SSomeL (sugar l)
Or l (Star r) | Just Refl <- testEquality l r -> SSomeR (sugar l)
Or l r -> SOr (sugar l) (sugar r)
Star r -> SStar (sugar r)
Then l r -> SThen (sugar l) (sugar r)
-- | Lift top-level 'Or's (i.e., those not under quantifiers)
liftOrs :: Sugar f r -> Seq (Some (Sugar f))
liftOrs =
\case
SEmpty -> Seq.singleton (Some SEmpty)
SFail -> Seq.empty
SLit x -> Seq.singleton (Some (SLit x))
r@(SOptL {}) -> Seq.singleton (Some r)
r@(SOptR {}) -> Seq.singleton (Some r)
SOr l r -> liftOrs l <> liftOrs r
r@(SSomeL {}) -> Seq.singleton (Some r)
r@(SSomeR {}) -> Seq.singleton (Some r)
SThen l r ->
Seq.fromList
[ Some (SThen x y)
| Some x <- Foldable.toList (liftOrs l)
, Some y <- Foldable.toList (liftOrs r)
]
SStar r -> Seq.singleton (Some (SStar r))
prettySugar ::
-- | Separator for 'SThen'
PP.Doc ann ->
(forall t. f t -> PP.Doc ann) ->
Sugar f r ->
PP.Doc ann
prettySugar sep f =
\case
SEmpty -> ""
SFail -> "∅"
SLit l -> f l
SOptL (SLit l) -> f l PP.<> "?"
SOptL l -> PP.parens (prettySugar sep f l) PP.<> "?"
SOptR (SLit r) -> f r PP.<> "?"
SOptR r -> PP.parens (prettySugar sep f r) PP.<> "?"
SOr l r -> PP.parens (prettySugar sep f l PP.<> "|" PP.<> prettySugar sep f r)
SSomeL (SLit l) -> f l PP.<> "+"
SSomeL l -> PP.parens (prettySugar sep f l) PP.<> "+"
SSomeR (SLit r) -> f r PP.<> "+"
SSomeR r -> PP.parens (prettySugar sep f r) PP.<> "+"
SStar (SLit l) -> f l PP.<> "*"
SStar r -> PP.parens (prettySugar sep f r) PP.<> "*"
SThen l r -> prettySugar sep f l PP.<> sep PP.<> prettySugar sep f r