distributors-0.6.0.0: src/Control/Lens/Grammar/Machine.hs
{- |
Module : Control.Lens.Grammar.Machine
Description : matching & transducers
Copyright : (C) 2026 - Eitan Chatav
License : BSD-style (see the file LICENSE)
Maintainer : Eitan Chatav <eitan.chatav@gmail.com>
Stability : provisional
Portability : non-portable
-}
module Control.Lens.Grammar.Machine
( -- * Matching
Matching (..)
-- * Transducer
, transducer
, parseForest
, languageSample
, expectNext
, unreachableRules
, Transducer (..)
, TransducerStep (..)
) where
import Control.Lens
import Control.Lens.Extras
import Control.Lens.Grammar.BackusNaur
import Control.Lens.Grammar.Boole
import Control.Lens.Grammar.Kleene
import Control.Lens.Grammar.Token
import Data.Foldable
import qualified Data.IntMap.Strict as IntMap
import Data.IntMap.Strict (IntMap)
import qualified Data.IntSet as IntSet
import Data.IntSet (IntSet)
import qualified Data.Map.Strict as Map
import Data.Map.Strict (Map)
import qualified Data.Set as Set
import Data.Set (Set)
import Data.Tree (Tree (..))
-- | Does a word match a pattern?
class Matching word pattern | pattern -> word where
(=~) :: word -> pattern -> Bool
infix 2 =~
-- instances
instance Categorized token
=> Matching [token] (Transducer token) where
word =~ et = acceptsChart n chart
where
(n, chart) = prefixGen et word
instance Categorized token
=> Matching [token] (Bnf (RegEx token)) where
word =~ bnf = word =~ transducer bnf
instance Categorized token
=> Matching [token] (RegEx token) where
word =~ pattern = word =~ liftBnf0 pattern
instance Matching s (APrism s t a b) where
word =~ pattern = is pattern word
{-| A `Transducer` is a tuple
@
T = (Σ, Δ, Q, I ⊆ Q, F ∈ Q, transition ⊆ Q × (Σ ∪ ∆) × Q, output ⊆ Q × ∆)
@
* @Σ@ is a (possibly infinite) set of terminal token classes, represented by `TokenClass`es.
* @Δ@ is a finite set of nonterminals, represented by the key set of `transducerRules`.
* @Q@ is a set of states, which is represented by the key set of `transducerRelations`.
* @I@ are initial states represented by `transducerStarts`.
* @F@ is a final state represented by @0@.
* @transition@ is a relation represented by `transducerRelations`
with `TransitionTokenClass` and `TransitionNonTerminal` transitions.
* @output@ is a relation represented by `transducerRelations` with `EmitNonTerminal` outputs.
-}
data Transducer token = Transducer
{ transducerRelations :: IntMap (TransducerStep token)
, transducerRules :: Map String (IntSet, Bool)
-- ^ an index into `transducerRelations` for nonterminals with precomputed nullability
, transducerStarts :: IntSet
-- ^ an index into `transducerRelations` for the starting rule
}
-- | A `TransducerStep` in a `Transducer`.
data TransducerStep token
= TransitionTokenClass (TokenClass token) IntSet
| TransitionNonTerminal String IntSet
| EmitNonTerminal String
{- | Compile a `RegEx`tended `Bnf` into a `Transducer`,
using a combination of Thompson's algorithm for regular expressions
and Earley's algorithm for context-free grammars. See Jim & Mandelbaum,
[Efficient Earley Parsing with Regular Right-hand Sides]
(http://trevorjim.com/papers/ldta-2009.pdf),
and McIlroy, [Enumerating the strings of regular languages]
(https://www.cs.dartmouth.edu/~doug/nfa.pdf).
A transducer is a form of finite state machine
that can be run in various ways like
`=~`, `expectNext`, `languageSample`, `parseForest` & `unreachableRules`.
-}
transducer :: Bnf (RegEx token) -> Transducer token
transducer (Bnf start rules) = Transducer
{ transducerRelations = IntMap.fromList allStates
, transducerRules = Map.fromList
[ ( n
, ( Map.findWithDefault IntSet.empty n firstsMap
, Set.member n nullSet
)
)
| n <- Map.keys ruleMap
]
, transducerStarts = startStates
}
where
ruleMap = foldr
(\(n, r) -> Map.insertWith (++) n [r]) Map.empty (toList rules)
rexNullable nm = \case
SeqEmpty -> True
NonTerminal n -> Set.member n nm
Sequence x y -> rexNullable nm x && rexNullable nm y
KleeneStar _ -> True
KleeneOpt _ -> True
KleenePlus x -> rexNullable nm x
RegExam (Alternate x y) -> rexNullable nm x || rexNullable nm y
RegExam (OneOf _) -> False
RegExam (NotOneOf _ _) -> False
ruleNames = Map.keys ruleMap
iterNull ns =
let ns' = Set.fromList
[ n
| n <- ruleNames
, any (rexNullable ns) (Map.findWithDefault [] n ruleMap)
]
in if ns == ns' then ns else iterNull ns'
nullSet = iterNull Set.empty
transducerAcceptId0 = 0
(finalMap, nextIdAfterFinals) =
foldl' alloc (Map.empty, transducerAcceptId0 + 1) ruleNames
where alloc (m, i) n = (Map.insert n i m, i + 1)
finalStatesList = [(finalMap Map.! n, EmitNonTerminal n) | n <- ruleNames]
(rulesStatesList, firstsMap, nextIdAfterRules) =
foldl' compileRule ([], Map.empty, nextIdAfterFinals) (Map.toList ruleMap)
where
compileRule (sts, fm, nid) (name, prods) =
let finalId = finalMap Map.! name
(newSts, newFirsts, nid') =
foldl' compileProd ([], IntSet.empty, nid) prods
compileProd (s, fs, i) prod =
let (f, st, i', _) =
thompson prod i (IntSet.singleton finalId)
in (s <> st, fs <> f, i')
in (sts <> newSts, Map.insert name newFirsts fm, nid')
(startFirsts, startStatesRaw, _, startBypass) =
thompson start nextIdAfterRules (IntSet.singleton transducerAcceptId0)
startStates =
startFirsts <> bypassStates startBypass (IntSet.singleton transducerAcceptId0)
allStates = finalStatesList <> rulesStatesList <> startStatesRaw
bypassStates True = id
bypassStates False = const IntSet.empty
thompson rex nextId dests = case rex of
SeqEmpty -> (IntSet.empty, [], nextId, True)
NonTerminal name ->
( IntSet.singleton nextId
, [(nextId, TransitionNonTerminal name dests)]
, nextId + 1
, Set.member name nullSet
)
Sequence rex0 rex1 ->
let
(firsts1, states1, nextId1, bypass1) = thompson rex1 nextId dests
(firsts0, states0, nextId0, bypass0) =
thompson rex0 nextId1 (firsts1 <> bypassStates bypass1 dests)
in
( firsts0 <> bypassStates bypass0 firsts1
, states0 <> states1
, nextId0
, bypass0 && bypass1
)
KleeneStar rex0 ->
let
(firsts, states, nextId', _) = thompson rex0 nextId (firsts <> dests)
in
(firsts, states, nextId', True)
KleeneOpt rex0 ->
let
(firsts, states, nextId', _) = thompson rex0 nextId dests
in
(firsts, states, nextId', True)
KleenePlus rex0 ->
let
(firsts, states, nextId', bypass) = thompson rex0 nextId (firsts <> dests)
in
(firsts, states, nextId', bypass)
RegExam (OneOf chars)
| Set.null chars -> (IntSet.empty, [], nextId, False)
| otherwise ->
( IntSet.singleton nextId
, [(nextId, TransitionTokenClass (TokenClass (OneOf chars)) dests)]
, nextId + 1
, False
)
RegExam (NotOneOf chars catTest) ->
( IntSet.singleton nextId
, [(nextId, TransitionTokenClass (TokenClass (NotOneOf chars catTest)) dests)]
, nextId + 1
, False
)
RegExam (Alternate rex0 rex1) ->
let
(firsts1, states1, nextId1, bypass1) = thompson rex1 nextId dests
(firsts0, states0, nextId0, bypass0) = thompson rex0 nextId1 dests
in
( firsts0 <> firsts1
, states0 <> states1
, nextId0
, bypass0 || bypass1
)
{- | The parse forest of a string of tokens. -}
parseForest
:: Categorized token
=> Transducer token
-> [token] -- ^ string
-> ([Tree (String, Int, Int, [token])], [token])
{- ^ parse forest & remaining unparsed tokens -}
parseForest et word = (concat (itemForests Set.empty Nothing 0 acceptedLen 0), drop acceptedLen word)
where
(n, chart) = prefixGen et word
relations = transducerRelations et
acceptedLen = maximum [j | j <- [0 .. n], acceptsChart j chart]
acceptedWord = take acceptedLen word
sliceAt start end = take (end - start) (drop start acceptedWord)
itemsAt j = IntMap.findWithDefault IntMap.empty j chart
ruleInfo name = Map.findWithDefault (IntSet.empty, False) name (transducerRules et)
edgesAt :: IntMap (IntMap [edge]) -> Int -> Int -> [edge]
edgesAt table pos stateId =
IntMap.findWithDefault [] stateId (IntMap.findWithDefault IntMap.empty pos table)
insertEdges :: edge -> IntSet -> IntMap [edge] -> IntMap [edge]
insertEdges edge dests acc = IntSet.foldr
(\stateId m -> IntMap.insertWith (++) stateId [edge] m)
acc
dests
scanBack = IntMap.fromList
[ (end, backRow (end - 1) input)
| (end, input) <- zip [1 .. acceptedLen] acceptedWord
]
where
backRow prev input = IntMap.foldrWithKey step IntMap.empty (itemsAt prev)
where
step prevState origins acc = case IntMap.lookup prevState relations of
Just (TransitionTokenClass cls dests) | tokenClass cls input ->
insertEdges (prevState, origins) dests acc
_ -> acc
completeBack = IntMap.fromList
[ (split, IntMap.foldrWithKey step IntMap.empty (itemsAt split))
| split <- [0 .. acceptedLen]
]
where
step caller origins acc = case IntMap.lookup caller relations of
Just (TransitionNonTerminal name dests) ->
insertEdges (caller, origins, name) dests acc
_ -> acc
ruleFinals = IntMap.foldrWithKey finalStates Map.empty relations
finalStates stateId step acc = case step of
EmitNonTerminal name -> Map.insert name stateId acc
_ -> acc
entryStates Nothing = transducerStarts et
entryStates (Just name) = fst (ruleInfo name)
ruleNullable = snd . ruleInfo
itemForests guards entry origin end stateId
| Set.member itemKey guards = []
| otherwise = baseForests <> scannedForests <> completedForests
where
itemKey = Left (entry, origin, end, stateId)
guards' = Set.insert itemKey guards
baseForests
| end == origin && IntSet.member stateId (entryStates entry) = [[]]
| otherwise = []
scannedForests
| end <= origin = []
| otherwise =
[ forest
| (prevState, origins) <- edgesAt scanBack end stateId
, IntSet.member origin origins
, let prev = end - 1
, forest <- itemForests guards' entry origin prev prevState
]
completedForests =
[ prefix <> [subtree]
| split <- [origin .. end]
, (caller, origins, name) <- edgesAt completeBack split stateId
, IntSet.member origin origins
, prefix <- itemForests guards' entry origin split caller
, subtree <- ruleTrees guards' name split end
]
ruleTrees guards name start end
| Set.member ruleKey guards = []
| otherwise = nullableTrees <> derivedTrees
where
ruleKey = Right (name, start, end)
guards' = Set.insert ruleKey guards
nullableTrees
| start == end && ruleNullable name = [Node (name, start, end, []) []]
| otherwise = []
derivedTrees = case Map.lookup name ruleFinals of
Nothing -> []
Just finalState ->
[ Node (name, start, end, sliceAt start end) subtrees
| subtrees <- itemForests guards' (Just name) start end finalState
]
prefixGen
:: Categorized token
=> Transducer token
-> [token]
-> (Int, IntMap (IntMap IntSet))
prefixGen et word = go 0 (initialChart et) word
where
go j chart [] = (j, chart)
go j chart (x : xs) =
let scanned = scanFrom j x chart
closed = closeChartAt et (j + 1) (IntMap.insert (j + 1) scanned chart)
in go (j + 1) closed xs
scanFrom j input chart = IntMap.foldrWithKey advance IntMap.empty eJ
where
eJ = IntMap.findWithDefault IntMap.empty j chart
advance s origs acc = case IntMap.lookup s (transducerRelations et) of
Just (TransitionTokenClass cls ds) | tokenClass cls input ->
IntSet.foldr
(\d -> IntMap.insertWith IntSet.union d origs) acc ds
_ -> acc
{- | What token is expected next?
The scanner frontier, `expectNext` returns the `TokenClass`
that can be scanned next after the given input prefix.
A `falseB` result means the current chart has no scanner transitions,
i.e. the prefix is a dead end for recognition.
-}
expectNext
:: Categorized token
=> Transducer token -> [token] {- ^ prefix -} -> TokenClass token
expectNext et word = anyB fst (scanClassOptions et n chart)
where
(n, chart) = prefixGen et word
{- |
Rule names that can never be entered from the start
expression — dead productions. A non-empty result is a grammar-hygiene
warning: those rules can be deleted without changing the recognized language.
-}
unreachableRules :: Transducer token -> Set String
unreachableRules et =
Map.keysSet (transducerRules et) `Set.difference` called
where
called = bfs (transducerStarts et) IntSet.empty Set.empty
bfs frontier seen calls
| IntSet.null fresh = calls
| otherwise = bfs next (seen <> fresh) calls'
where
fresh = IntSet.difference frontier seen
(next, calls') = IntSet.foldr step (IntSet.empty, calls) fresh
step s (acc, cs) = case IntMap.lookup s (transducerRelations et) of
Just (TransitionTokenClass _ ds) -> (acc <> ds, cs)
Just (TransitionNonTerminal name ds) ->
let firsts = maybe IntSet.empty fst (Map.lookup name (transducerRules et))
in (acc <> ds <> firsts, Set.insert name cs)
Just (EmitNonTerminal _) -> (acc, cs)
Nothing -> (acc, cs)
{- |
`languageSample` lazily produces all words in a language from shortest to longest.
However since `TokenClass`es can resolve to infinite sets of tokens,
and the relevant case of `Char` tokens while not infinite is huge,
it samples tokens in an `Applicative` `TokenAlgebra`.
-}
languageSample
:: (TokenAlgebra token (f token), Applicative f)
=> Transducer token -- ^ transducer
-> f [[token]]
languageSample et = sequenceA (fmap sampleWord classWords)
where
classWords = enumerateByLength [(0, [], initialChart et)] Set.empty
sampleWord = traverse tokenClass . reverse
enumerateByLength [] _ = []
enumerateByLength frontier seen =
let
(accepted, seen') = acceptedAtFrontier frontier seen
next = concatMap expand frontier
in accepted <> enumerateByLength next seen'
acceptedAtFrontier frontier seen0 =
let (acceptedRev, seen') = foldl' step ([], seen0) frontier
in (reverse acceptedRev, seen')
where
step (acc, seen) (j, revWord, chart)
| acceptsChart j chart =
if Set.member revWord seen
then (acc, seen)
else (revWord : acc, Set.insert revWord seen)
| otherwise = (acc, seen)
expand (j, revWord, chart) =
[ (j + 1, cls : revWord, nextChart)
| (cls, nextChart) <- scanClassOptions et j chart
]
initialChart
:: Transducer token
-> IntMap (IntMap IntSet)
initialChart et = closeChartAt et 0 (IntMap.singleton 0 initialE0)
where
initialE0 = IntMap.fromList
[ (s, IntSet.singleton 0) | s <- IntSet.toList (transducerStarts et) ]
-- Accept iff (q_accept, 0) is in E_n.
acceptsChart
:: Int
-> IntMap (IntMap IntSet)
-> Bool
acceptsChart j chart = IntSet.member 0 acceptOrigins
where
eJ = IntMap.findWithDefault IntMap.empty j chart
acceptOrigins = IntMap.findWithDefault IntSet.empty 0 eJ
-- Group all scanner moves from E_j by token class; each result also carries the
-- closed successor chart at j+1.
scanClassOptions
:: Categorized token
=> Transducer token
-> Int
-> IntMap (IntMap IntSet)
-> [(TokenClass token, IntMap (IntMap IntSet))]
scanClassOptions et j chart =
[ (cls, closeChartAt et (j + 1) (IntMap.insert (j + 1) scanned chart))
| (cls, scanned) <- Map.toAscList grouped
]
where
grouped = IntMap.foldrWithKey advance Map.empty eJ
eJ = IntMap.findWithDefault IntMap.empty j chart
advance s origs acc = case IntMap.lookup s (transducerRelations et) of
Just (TransitionTokenClass cls ds) ->
Map.insertWith (IntMap.unionWith IntSet.union) cls scanned acc
where
scanned = IntSet.foldr
(\d -> IntMap.insertWith IntSet.union d origs) IntMap.empty ds
_ -> acc
closeChartAt
:: Transducer token
-> Int
-> IntMap (IntMap IntSet)
-> IntMap (IntMap IntSet)
closeChartAt et j initialChart0 = loop initialWork initialChart0 IntMap.empty
where
initialEJ = IntMap.findWithDefault IntMap.empty j initialChart0
initialWork =
[ (s, i) | (s, os) <- IntMap.toList initialEJ, i <- IntSet.toList os ]
-- For fixed i < j, E_i does not change while closing E_j. Cache an index
-- from nonterminal name to caller origins/continuations to speed completion.
-- IntMap key: origin index i
-- Map key: nonterminal name
-- Value: list of (caller origins, continuation destinations)
-- Earley closure at E_j: apply predict/complete until fixed point.
loop [] chart _ = chart
loop ((s, i) : rest) chart callerCache = case IntMap.lookup s (transducerRelations et) of
Just (TransitionNonTerminal name ds) ->
let
(firsts, isNull) = Map.findWithDefault
(IntSet.empty, False) name (transducerRules et)
predItems = [(f, j) | f <- IntSet.toList firsts]
nullItems =
if isNull then [(d, i) | d <- IntSet.toList ds] else []
(chart', new) = addEarleyItems (predItems <> nullItems) chart
in loop (new <> rest) chart' callerCache
Just (EmitNonTerminal name) ->
let
(ixed, callerCache') = callerEntries i chart callerCache
callerRows = Map.findWithDefault [] name ixed
completions =
[ (d, i')
| (os, ds) <- callerRows
, i' <- IntSet.toList os
, d <- IntSet.toList ds
]
(chart', new) = addEarleyItems completions chart
in loop (new <> rest) chart' callerCache'
_ -> loop rest chart callerCache
callerEntries i chart callerCache
-- E_j mutates during closure, so do not cache index for i == j.
| i == j = (buildCallerIndex (IntMap.findWithDefault IntMap.empty i chart), callerCache)
| otherwise = case IntMap.lookup i callerCache of
Just ixed -> (ixed, callerCache)
Nothing ->
let ixed = buildCallerIndex (IntMap.findWithDefault IntMap.empty i chart)
in (ixed, IntMap.insert i ixed callerCache)
buildCallerIndex eI = IntMap.foldrWithKey step Map.empty eI
where
step t os acc = case IntMap.lookup t (transducerRelations et) of
Just (TransitionNonTerminal n ds) ->
Map.insertWith (++) n [(os, ds)] acc
_ -> acc
addEarleyItems items chart = foldl' ins (chart, []) items
where
ins (acc, new) (state, origin) =
let
eJ = IntMap.findWithDefault IntMap.empty j acc
os = IntMap.findWithDefault IntSet.empty state eJ
in if IntSet.member origin os
then (acc, new)
else
let
eJ' = IntMap.insert state (IntSet.insert origin os) eJ
acc' = IntMap.insert j eJ' acc
in (acc', (state, origin) : new)