LR-demo-0.0.20251105: src/CFG.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-- | Context-free grammars: syntax and grammar folds.
module CFG where
import Control.Monad.Except
import Control.Monad.State
import qualified Data.Foldable as Fold
import qualified Data.List as List
import Data.IntMap (IntMap)
import qualified Data.IntMap as IntMap
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Maybe (mapMaybe)
import Data.Function (on)
import Data.Tuple (swap)
import Data.Semigroup (Semigroup(..))
-- uses microlens-platform
import Lens.Micro
import Lens.Micro.Extras (view)
import Lens.Micro.TH (makeLenses)
import Saturation
import SetMaybe (SetMaybe)
import qualified SetMaybe
-- | A grammar over non-terminal names x, rulenames r and an alphabet t
-- consists of definitions of the nonterminals, represented as Ints.
data Grammar' x r t = Grammar
{ _grmNumNT :: Int -- ^ Number of non-terminals.
, _grmNTDict :: Map x NTId -- ^ Names-to-number map for non-terminals.
, _grmNTDefs :: IntMap (NTDef' x r t) -- ^ Definitions of non-terminals.
}
emptyGrammar :: Grammar' x r t
emptyGrammar = Grammar 0 Map.empty IntMap.empty
-- | A nonterminal is defined by a list of alternatives.
data NTDef' x r t = NTDef { _ntName :: x, _ntDef :: [Alt' x r t] }
-- | Each alternative is a rule name plus a sentential form.
data Alt' x r t = Alt r (Form' x t)
deriving (Eq, Ord, Show)
-- | A sentential form is a string of symbols.
newtype Form' x t = Form { theForm :: [Symbol' x t] }
deriving (Eq, Ord, Show)
-- | A symbol is a terminal or a non-terminal.
data Symbol' x t
= Term t
| NonTerm (NT' x)
deriving (Eq, Ord, Show)
-- | Non-terminals are natural numbers.
-- We store the original name for printing purposes.
--
data NT' x = NT { ntNum :: NTId, ntNam :: x }
deriving (Show)
instance Eq (NT' x) where (==) = (==) `on` ntNum
instance Ord (NT' x) where compare = compare `on` ntNum
type NTId = Int
-- Lenses
makeLenses ''Grammar'
makeLenses ''NTDef'
-- | Disregarding 'NTName', we can join non-terminal definitions.
instance (Show x, Eq x) => Semigroup (NTDef' x r t) where
NTDef x alts <> NTDef x' alts'
| x == x' = NTDef x $ alts ++ alts'
| otherwise = error $ unwords $
[ "non-terminal names do not match:" ] ++ map show [x, x']
-- ** Converting 'NTId' back to name.
-- | Decoration of something with a NT printing dictionary.
data WithNTNames x a = WithNTNames
{ _wntNames :: IntMap x -- ^ Number-to-names map for non-terminals.
, _wntThing :: a -- ^ The decorated thing.
}
makeLenses ''WithNTNames
class GetNTNames x a where
getNTNames :: a -> IntMap x
instance GetNTNames x (WithNTNames x a) where
getNTNames = (^. wntNames)
instance GetNTNames x (Grammar' x r t) where
getNTNames g = (^. ntName) <$> g ^. grmNTDefs
-- * Generic grammar folds.
-- The class-based approach did not go well with Haskell's
-- instance inference.
--
-- class GrmAlg t a where
-- gaTerminal :: t -> a -- ^ Single terminal.
-- gaZero :: a -- ^ Empty language.
-- gaPlus :: a -> a -> a -- ^ Language union.
-- gaEps :: a -- ^ Language of the empty word.
-- gaConcat :: a -> a -> a -- ^ Language concatenation.
-- class GrmFold t a b where
-- grmFold :: GrmAlg t a => (NT -> a) -> b -> a
-- | A grammar algebra provides an implementation for
-- the operations constituting CFGs.
data GrmAlg r t a = GrmAlg
{ gaTerminal :: t -> a -- ^ Single terminal.
, gaZero :: a -- ^ Empty language.
, gaPlus :: a -> a -> a -- ^ Language union.
, gaEps :: a -- ^ Language of the empty word.
, gaConcat :: a -> a -> a -- ^ Language concatenation.
, gaLabel :: r -> a -> a -- ^ Labelled language.
}
-- | @n@-ary concatenation, with a special case for empty concatenation.
gaProduct :: GrmAlg r t a -> [a] -> a
gaProduct ga [] = gaEps ga
gaProduct ga as = foldl1 (gaConcat ga) as
-- | @n@-ary alternative, with a special case for empty language.
gaSum :: GrmAlg r t a -> [a] -> a
gaSum ga [] = gaZero ga
gaSum ga as = foldl1 (gaPlus ga) as
-- | Generic fold over a grammar.
class GrmFold r t a b where
grmFold :: GrmAlg r t a -> (NTId -> a) -> b -> a
instance GrmFold r t a (NT' x) where
grmFold ga env x = env $ ntNum x
instance GrmFold r t a (Symbol' r' t) where
grmFold ga env = \case
Term t -> gaTerminal ga t
NonTerm x -> env $ ntNum x
instance GrmFold r t a (Form' r' t) where
grmFold ga env (Form alpha) = gaProduct ga $ map (grmFold ga env) alpha
instance GrmFold r t a (Alt' x r t) where
grmFold ga env (Alt r alpha) = gaLabel ga r (grmFold ga env alpha)
instance GrmFold r t a (NTDef' x r t) where
grmFold ga env (NTDef _x alts) = gaSum ga $ map (grmFold ga env) alts
-- | Computing properties of non-terminals by saturation.
-- The iteration is needed to handle the recursion inherent in CFGs.
-- Requires a bounded lattice @a@.
grmIterate :: forall r t a x . (Eq a, Ord a)
=> GrmAlg r t a -- ^ Grammar algebra.
-> Grammar' x r t -- ^ Grammar.
-> a -- ^ Default/start value.
-> Maybe a -- ^ Best value (if it exists).
-> IntMap a -- ^ Final value for each non-terminal.
grmIterate ga grm@(Grammar n dict defs) bot mtop
= IntMap.map fst
$ saturate (\ gs -> IntMap.traverseWithKey (step gs) gs)
$ IntMap.map (bot,) defs
where
step :: IntMap (a, NTDef' x r t)
-> NTId
-> (a, NTDef' x r t)
-> Change (a, NTDef' x r t)
step gs i d@(a, def)
| Just a /= mtop, let a' = grmFold ga env def, a' > a = do
dirty -- change!
return (a', def)
| otherwise = return d -- no change
where
env j = fst $ IntMap.findWithDefault (error "grmIterate") j gs
-- * Guardedness.
newtype Guarded = Guarded { getGuarded :: Bool }
deriving (Eq, Ord, Show, Bounded) -- False < True
guardedAlg :: GrmAlg r t Guarded
guardedAlg = GrmAlg
{ gaTerminal = const maxBound -- Yes. A terminal is guarded.
, gaZero = maxBound -- Yes. (A bit arbitrary, but consistent with gaPlus.)
, gaPlus = min -- All alternatives need to be guarded.
, gaEps = maxBound -- Empty language is guarded! (Outlier!)
, gaConcat = max -- One factor needs to be guarded.
, gaLabel = const id -- Labels do not change the game.
}
computeGuardedness :: Grammar' x r t -> IntMap Guarded
computeGuardedness grm = grmIterate guardedAlg grm minBound (Just maxBound)
-- * Nullability.
newtype Nullable = Nullable { getNullable :: Bool }
deriving (Eq, Ord, Show, Bounded) -- False < True
nullableAlg :: GrmAlg r t Nullable
nullableAlg = GrmAlg
{ gaTerminal = const minBound -- No. A terminal is not nullable.
, gaZero = minBound -- No.
, gaPlus = max -- One alternative suffices.
, gaEps = maxBound -- Yes. Empty language is exactly nullable.
, gaConcat = min -- All factor must be nullable.
, gaLabel = const id -- Labels do not change the game.
}
computeNullable :: Grammar' x r t -> IntMap Nullable
computeNullable grm = grmIterate nullableAlg grm minBound (Just maxBound)
-- * First sets
newtype First t = First { getFirst :: SetMaybe t }
deriving (Eq, Ord, Show)
-- | Rules to compute first sets.
--
-- FIRST(a) = {a}
-- FIRST(ε) = {ε}
-- FIRST(αβ) = FIRST(α) ∪ (NULLABLE(α) ⇒ FIRST(β))
-- FIRST(α+β) = FIRST(α) ∪ FIRST(β)
firstAlg :: Ord t => GrmAlg r t (First t)
firstAlg = GrmAlg
{ gaTerminal = First . SetMaybe.singleton . Just
, gaEps = First $ SetMaybe.singleton Nothing
, gaZero = First $ SetMaybe.empty
, gaPlus = \ (First s) (First s') -> First $ SetMaybe.union s s'
, gaConcat = concatFirst
, gaLabel = const id
}
-- | Empty FIRST set.
emptyFirst :: First t
emptyFirst = First $ SetMaybe.empty
-- | FIRST(αβ) = FIRST(α) ∪ (NULLABLE(α) ⇒ FIRST(β)).
concatFirst :: Ord t => First t -> First t -> First t
concatFirst (First s) (First s')
| SetMaybe.member Nothing s = First $ SetMaybe.union s s'
| otherwise = First s
-- | FIRST sets for all non-terminals.
type FirstSets t = IntMap (First t)
-- | Compute FIRST sets for all non-terminals.
computeFirst :: Ord t => Grammar' x r t -> FirstSets t
computeFirst grm = grmIterate firstAlg grm emptyFirst Nothing
-- Ambiguous: r
-- firstSet :: (GrmFold r t (First t) b, Ord t) => FirstSets t -> b -> First t
firstSet :: (Ord t) => FirstSets t -> Form' r t -> First t
firstSet fs = grmFold firstAlg (\ x -> IntMap.findWithDefault err x fs)
where
err = error $ "CFG.firstSet: undefined nonterminal"
-- | Enriched grammar.
data EGrammar' x r t = EGrammar
{ _eGrm :: Grammar' x r t -- ^ CFG.
, _eStart :: NT' x -- ^ Start symbol.
, _eFirst :: FirstSets t -- ^ Precomputed FIRST sets.
}
makeLenses ''EGrammar'
makeEGrammar :: Ord t => Grammar' x r t -> NT' x -> EGrammar' x r t
makeEGrammar grm start = EGrammar grm start $ computeFirst grm
instance GetNTNames x (EGrammar' x r t) where
getNTNames = getNTNames . _eGrm