packages feed

typed-peg 0.1.0.0 → 0.2.0.0

raw patch · 24 files changed

+3435/−261 lines, 24 filesdep +bytestringdep +criteriondep +deepseqdep ~basedep ~template-haskellPVP ok

version bump matches the API change (PVP)

Dependencies added: bytestring, criterion, deepseq, megaparsec, text

Dependency ranges changed: base, template-haskell

API changes (from Hackage documentation)

- PEG.Member: instance (TypeError ...) => PEG.Member.KnownMember s '[] ty a
- PEG.Member: instance (s GHC.Types.~ t, e GHC.Types.~ 'PEG.Type.EnvEntry ty a) => PEG.Member.KnownMember' 'GHC.Types.True s ('(t, e) : rest) ty a
- PEG.Member: instance PEG.Member.KnownMember s rest ty a => PEG.Member.KnownMember' 'GHC.Types.False s ('(t, e) : rest) ty a
- PEG.Member: instance PEG.Member.KnownMember' (PEG.TyLevel.SymEq s t) s ('(t, e) : rest) ty a => PEG.Member.KnownMember s ('(t, e) : rest) ty a
- PEG.Parse: columns :: Int -> String -> Input
- PEG.Parse: eval :: forall (env :: Env) (ty :: Ty) a. Rules env env -> PExp env ty a -> RelD -> PState -> Maybe (a, PState)
- PEG.Parse: instance GHC.Classes.Eq a => GHC.Classes.Eq (PEG.Parse.Result a)
- PEG.Parse: instance GHC.Show.Show a => GHC.Show.Show (PEG.Parse.Result a)
- PEG.Parse: type Input = [(Char, Int)]
- PEG.Semantics.Simple: class Stream d where {
- PEG.Semantics.Simple: instance PEG.Semantics.Simple.Stream [a]
- PEG.Syntax: instance GHC.Base.Functor (PEG.Syntax.PExp env ty)
+ PEG.CharSet: CharSet :: !Bool -> !Word64 -> !Word64 -> !Word64 -> !Word64 -> ![(Char, Char)] -> CharSet
+ PEG.CharSet: [csB0] :: CharSet -> !Word64
+ PEG.CharSet: [csB1] :: CharSet -> !Word64
+ PEG.CharSet: [csB2] :: CharSet -> !Word64
+ PEG.CharSet: [csB3] :: CharSet -> !Word64
+ PEG.CharSet: [csNeg] :: CharSet -> !Bool
+ PEG.CharSet: [csWide] :: CharSet -> ![(Char, Char)]
+ PEG.CharSet: anyCS :: CharSet
+ PEG.CharSet: complementCS :: CharSet -> CharSet
+ PEG.CharSet: data CharSet
+ PEG.CharSet: fromList :: [Char] -> CharSet
+ PEG.CharSet: fromRanges :: [(Char, Char)] -> CharSet
+ PEG.CharSet: instance GHC.Classes.Eq PEG.CharSet.CharSet
+ PEG.CharSet: instance GHC.Show.Show PEG.CharSet.CharSet
+ PEG.CharSet: memberCS :: Char -> CharSet -> Bool
+ PEG.CharSet: notInRanges :: [(Char, Char)] -> CharSet
+ PEG.CharSet: nullCS :: CharSet -> Bool
+ PEG.CharSet: singletonCS :: Char -> CharSet
+ PEG.Indent: [rdTotal] :: RelD -> !Bool
+ PEG.Member: instance (TypeError ...) => PEG.Member.KnownMember s '[] a
+ PEG.Member: instance (s GHC.Types.~ t) => PEG.Member.KnownMemberStep 'GHC.Types.EQ s ('(t, 'PEG.Type.EnvEntry ty a) : rest) a
+ PEG.Member: instance PEG.Member.KnownMember s rest a => PEG.Member.KnownMemberStep 'GHC.Types.GT s ('(t, e) : rest) a
+ PEG.Member: instance PEG.Member.KnownMember s rest a => PEG.Member.KnownMemberStep 'GHC.Types.LT s ('(t, e) : rest) a
+ PEG.Member: instance PEG.Member.KnownMemberStep (GHC.TypeLits.Internal.CmpSymbol s t) s ('(t, e) : rest) a => PEG.Member.KnownMember s ('(t, e) : rest) a
+ PEG.Parse: [stCol] :: PState s -> !Int
+ PEG.Parse: [stOff] :: PState s -> !Int
+ PEG.Parse: compileGrammar :: forall s (env :: Env) (ty :: Ty) a. Stream s => Int -> Grammar s env ty a -> Step s a
+ PEG.Parse: instance (GHC.Classes.Eq a, GHC.Classes.Eq s) => GHC.Classes.Eq (PEG.Parse.Result s a)
+ PEG.Parse: instance (GHC.Show.Show a, GHC.Show.Show s) => GHC.Show.Show (PEG.Parse.Result s a)
+ PEG.Parse: nextCol :: Int -> Int -> Char -> Int
+ PEG.Parse: type Res s a = (# (# #) | (# a, PState s #) #)
+ PEG.Parse: type Step s a = RelD -> PState s -> Res s a
+ PEG.Semantics.Simple: class SimpleStream d where {
+ PEG.Semantics.Simple: instance PEG.Semantics.Simple.SimpleStream [a]
+ PEG.Stream: chunkToString :: Stream s => s -> String
+ PEG.Stream: class Stream s
+ PEG.Stream: consS :: Stream s => Char -> s -> s
+ PEG.Stream: foldlS' :: Stream s => (b -> Char -> b) -> b -> s -> b
+ PEG.Stream: instance PEG.Stream.Stream Data.ByteString.Internal.Type.ByteString
+ PEG.Stream: instance PEG.Stream.Stream Data.ByteString.Lazy.Internal.ByteString
+ PEG.Stream: instance PEG.Stream.Stream Data.Text.Internal.Lazy.Text
+ PEG.Stream: instance PEG.Stream.Stream Data.Text.Internal.Text
+ PEG.Stream: instance PEG.Stream.Stream [GHC.Types.Char]
+ PEG.Stream: lengthS :: Stream s => s -> Int
+ PEG.Stream: nullS :: Stream s => s -> Bool
+ PEG.Stream: packString :: Stream s => String -> s
+ PEG.Stream: spanS :: Stream s => (Char -> Bool) -> s -> (s, s)
+ PEG.Stream: takeS :: Stream s => Int -> s -> s
+ PEG.Stream: unconsS :: Stream s => s -> (# (# #) | (# Char, s #) #)
+ PEG.Syntax: [Sat] :: forall s (env :: Env). !CharSet -> PExp s env ('MkTy 'False ('[] :: [Symbol])) Char
+ PEG.Syntax: [Span1] :: forall s (env :: Env). !CharSet -> PExp s env ('MkTy 'False ('[] :: [Symbol])) s
+ PEG.Syntax: [Span] :: forall s (env :: Env). !CharSet -> PExp s env ('MkTy 'True ('[] :: [Symbol])) s
+ PEG.Syntax: [Str] :: forall s (env :: Env). String -> PExp s env ('MkTy 'False ('[] :: [Symbol])) [Char]
+ PEG.Syntax: charClass :: forall s (env :: Env). [(Char, Char)] -> PExp s env ('MkTy 'False ('[] :: [Symbol])) Char
+ PEG.Syntax: instance GHC.Base.Functor (PEG.Syntax.PExp s env ty)
+ PEG.Syntax: notCharClass :: forall s (env :: Env). [(Char, Char)] -> PExp s env ('MkTy 'False ('[] :: [Symbol])) Char
+ PEG.Syntax: sat :: forall s (env :: Env). CharSet -> PExp s env ('MkTy 'False ('[] :: [Symbol])) Char
+ PEG.Syntax: spanOf :: forall s (env :: Env). CharSet -> PExp s env ('MkTy 'True ('[] :: [Symbol])) s
+ PEG.Syntax: spanOf1 :: forall s (env :: Env). CharSet -> PExp s env ('MkTy 'False ('[] :: [Symbol])) s
+ PEG.Syntax: type family NTGo (n :: Symbol) (t :: Ty) :: Ty
- PEG.Grammar: [Grammar] :: forall (env :: Env) (startTy :: Ty) startA. Acyclic env => Rules env env -> PExp env startTy startA -> Grammar env startTy startA
+ PEG.Grammar: [Grammar] :: forall (env :: Env) s (startTy :: Ty) startA. Acyclic env => Rules s env env -> PExp s env startTy startA -> Grammar s env startTy startA
- PEG.Grammar: [RCons] :: forall (s :: Symbol) (env :: Env) (ty :: Ty) a (rest :: [(Symbol, EnvEntry)]). Name s -> PExp env ty a -> Rules env rest -> Rules env ('(s, 'EnvEntry ty a) ': rest)
+ PEG.Grammar: [RCons] :: forall (n :: Symbol) s (env :: Env) (ty :: Ty) a (rest :: [(Symbol, EnvEntry)]). Name n -> PExp s env ty a -> Rules s env rest -> Rules s env ('(n, 'EnvEntry ty a) ': rest)
- PEG.Grammar: [RNil] :: forall (env :: Env). Rules env ('[] :: [(Symbol, EnvEntry)])
+ PEG.Grammar: [RNil] :: forall s (env :: Env). Rules s env ('[] :: [(Symbol, EnvEntry)])
- PEG.Grammar: data Grammar (env :: Env) (startTy :: Ty) startA
+ PEG.Grammar: data Grammar s (env :: Env) (startTy :: Ty) startA
- PEG.Grammar: data Rules (env :: Env) (defs :: Env)
+ PEG.Grammar: data Rules s (env :: Env) (defs :: Env)
- PEG.Indent: RelD :: String -> Interval -> (Int -> Int) -> (Int -> Bound) -> (Int -> Int) -> (Int -> Bound) -> Bound -> Bound -> Bound -> Bound -> RelD
+ PEG.Indent: RelD :: String -> !Bool -> Interval -> (Int -> Int) -> (Int -> Bound) -> (Int -> Int) -> (Int -> Bound) -> Bound -> Bound -> Bound -> Bound -> RelD
- PEG.Member: [Here] :: forall (s :: Symbol) (ty :: Ty) a (rest :: [(Symbol, EnvEntry)]). Member s ('(s, 'EnvEntry ty a) ': rest) ty a
+ PEG.Member: [Here] :: forall (s :: Symbol) (ty :: Ty) a (rest :: [(Symbol, EnvEntry)]). Member s ('(s, 'EnvEntry ty a) ': rest) a
- PEG.Member: [There] :: forall (s :: Symbol) (rest :: [(Symbol, EnvEntry)]) (ty :: Ty) a (e :: (Symbol, EnvEntry)). Member s rest ty a -> Member s (e ': rest) ty a
+ PEG.Member: [There] :: forall (s :: Symbol) (rest :: [(Symbol, EnvEntry)]) a (e :: (Symbol, EnvEntry)). Member s rest a -> Member s (e ': rest) a
- PEG.Member: class KnownMember (s :: Symbol) (env :: Env) (ty :: Ty) a
+ PEG.Member: class KnownMember (s :: Symbol) (env :: Env) a
- PEG.Member: data Member (s :: Symbol) (env :: Env) (ty :: Ty) a
+ PEG.Member: data Member (s :: Symbol) (env :: Env) a
- PEG.Member: member :: KnownMember s env ty a => Member s env ty a
+ PEG.Member: member :: KnownMember s env a => Member s env a
- PEG.Parse: Fail :: Result a
+ PEG.Parse: Fail :: Result s a
- PEG.Parse: OK :: a -> String -> String -> Result a
+ PEG.Parse: OK :: a -> s -> s -> Result s a
- PEG.Parse: PState :: Input -> !Interval -> !Bool -> PState
+ PEG.Parse: PState :: !s -> !Int -> !Int -> !Interval -> !Bool -> PState s
- PEG.Parse: [stAlign] :: PState -> !Bool
+ PEG.Parse: [stAlign] :: PState s -> !Bool
- PEG.Parse: [stCands] :: PState -> !Interval
+ PEG.Parse: [stCands] :: PState s -> !Interval
- PEG.Parse: [stInput] :: PState -> Input
+ PEG.Parse: [stInput] :: PState s -> !s
- PEG.Parse: data PState
+ PEG.Parse: data PState s
- PEG.Parse: data Result a
+ PEG.Parse: data Result s a
- PEG.Parse: parse :: forall (env :: Env) (ty :: Ty) a. Grammar env ty a -> String -> Result a
+ PEG.Parse: parse :: forall s (env :: Env) (ty :: Ty) a. Stream s => Grammar s env ty a -> s -> Result s a
- PEG.Parse: parseWith :: forall (env :: Env) (ty :: Ty) a. Opts -> Grammar env ty a -> String -> Result a
+ PEG.Parse: parseWith :: forall s (env :: Env) (ty :: Ty) a. Stream s => Opts -> Grammar s env ty a -> s -> Result s a
- PEG.Semantics.Simple: anyChar :: Stream d => PExp d (Elem d)
+ PEG.Semantics.Simple: anyChar :: SimpleStream d => PExp d (Elem d)
- PEG.Semantics.Simple: char :: (Eq (Elem d), Stream d) => Elem d -> PExp d (Elem d)
+ PEG.Semantics.Simple: char :: (Eq (Elem d), SimpleStream d) => Elem d -> PExp d (Elem d)
- PEG.Semantics.Simple: eof :: Stream d => PExp d ()
+ PEG.Semantics.Simple: eof :: SimpleStream d => PExp d ()
- PEG.Semantics.Simple: satisfy :: Stream d => (Elem d -> Bool) -> PExp d (Elem d)
+ PEG.Semantics.Simple: satisfy :: SimpleStream d => (Elem d -> Bool) -> PExp d (Elem d)
- PEG.Syntax: (.>>.) :: forall (env :: Env) (t1 :: Ty) a (t2 :: Ty) b. PExp env t1 a -> PExp env t2 b -> PExp env (SeqTy t1 t2) b
+ PEG.Syntax: (.>>.) :: forall s (env :: Env) (t1 :: Ty) a (t2 :: Ty) b. PExp s env t1 a -> PExp s env t2 b -> PExp s env (SeqTy t1 t2) b
- PEG.Syntax: (.||.) :: forall (env :: Env) (t1 :: Ty) a (t2 :: Ty). PExp env t1 a -> PExp env t2 a -> PExp env (ChoiceTy t1 t2) a
+ PEG.Syntax: (.||.) :: forall s (env :: Env) (t1 :: Ty) a (t2 :: Ty). PExp s env t1 a -> PExp s env t2 a -> PExp s env (ChoiceTy t1 t2) a
- PEG.Syntax: (<$>.) :: forall a b (env :: Env) (ty :: Ty). (a -> b) -> PExp env ty a -> PExp env ty b
+ PEG.Syntax: (<$>.) :: forall a b s (env :: Env) (ty :: Ty). (a -> b) -> PExp s env ty a -> PExp s env ty b
- PEG.Syntax: (<*>.) :: forall (env :: Env) (t1 :: Ty) a b (t2 :: Ty). PExp env t1 (a -> b) -> PExp env t2 a -> PExp env (SeqTy t1 t2) b
+ PEG.Syntax: (<*>.) :: forall s (env :: Env) (t1 :: Ty) a b (t2 :: Ty). PExp s env t1 (a -> b) -> PExp s env t2 a -> PExp s env (SeqTy t1 t2) b
- PEG.Syntax: Name :: Name (s :: Symbol)
+ PEG.Syntax: Name :: Name (n :: Symbol)
- PEG.Syntax: [Align] :: forall (env :: Env) (ty :: Ty) a. PExp env ty a -> PExp env ty a
+ PEG.Syntax: [Align] :: forall s (env :: Env) (ty :: Ty) a. PExp s env ty a -> PExp s env ty a
- PEG.Syntax: [AnyChar] :: forall (env :: Env). PExp env ('MkTy 'False ('[] :: [Symbol])) Char
+ PEG.Syntax: [AnyChar] :: forall s (env :: Env). PExp s env ('MkTy 'False ('[] :: [Symbol])) Char
- PEG.Syntax: [Choice] :: forall (env :: Env) (t1 :: Ty) a (t2 :: Ty). PExp env t1 a -> PExp env t2 a -> PExp env ('MkTy (Or (Nullable t1) (Nullable t2)) (Union (First t1) (First t2))) a
+ PEG.Syntax: [Choice] :: forall s (env :: Env) (t1 :: Ty) a (t2 :: Ty). PExp s env t1 a -> PExp s env t2 a -> PExp s env ('MkTy (Or (Nullable t1) (Nullable t2)) (Union (First t1) (First t2))) a
- PEG.Syntax: [Indent] :: forall (n :: Symbol) (env :: Env) (ty :: Ty) a. Rel n -> PExp env ty a -> PExp env ty a
+ PEG.Syntax: [Indent] :: forall (n :: Symbol) s (env :: Env) (ty :: Ty) a. Rel n -> PExp s env ty a -> PExp s env ty a
- PEG.Syntax: [Map] :: forall a1 a (env :: Env) (ty :: Ty). (a1 -> a) -> PExp env ty a1 -> PExp env ty a
+ PEG.Syntax: [Map] :: forall a1 a s (env :: Env) (ty :: Ty). (a1 -> a) -> PExp s env ty a1 -> PExp s env ty a
- PEG.Syntax: [NT] :: forall (s :: Symbol) (env :: Env). (KnownSymbol s, KnownMember s env (TyOf (Lookup s env)) (ResOf (Lookup s env))) => Name s -> PExp env ('MkTy (Nullable (TyOf (Lookup s env))) (ConsIfAbsent s (First (TyOf (Lookup s env))))) (ResOf (Lookup s env))
+ PEG.Syntax: [NT] :: forall (n :: Symbol) (ty1 :: Ty) s (env :: Env) a. (KnownSymbol n, Lookup n env ~ 'EnvEntry ty1 a, KnownMember n env a) => Name n -> PExp s env (NTGo n ty1) a
- PEG.Syntax: [Not] :: forall (env :: Env) (n :: Bool) (f :: [Symbol]) a1. PExp env ('MkTy n f) a1 -> PExp env ('MkTy 'True f) ()
+ PEG.Syntax: [Not] :: forall s (env :: Env) (n :: Bool) (f :: [Symbol]) a1. PExp s env ('MkTy n f) a1 -> PExp s env ('MkTy 'True f) ()
- PEG.Syntax: [Position] :: forall (n :: Symbol) (env :: Env) (ty :: Ty) a. Rel n -> PExp env ty a -> PExp env ty a
+ PEG.Syntax: [Position] :: forall (n :: Symbol) s (env :: Env) (ty :: Ty) a. Rel n -> PExp s env ty a -> PExp s env ty a
- PEG.Syntax: [Pure] :: forall a (env :: Env). a -> PExp env ('MkTy 'True ('[] :: [Symbol])) a
+ PEG.Syntax: [Pure] :: forall a s (env :: Env). a -> PExp s env ('MkTy 'True ('[] :: [Symbol])) a
- PEG.Syntax: [Seq] :: forall (env :: Env) (t1 :: Ty) a1 a (t2 :: Ty). PExp env t1 (a1 -> a) -> PExp env t2 a1 -> PExp env ('MkTy (And (Nullable t1) (Nullable t2)) (Union (First t1) (If (Nullable t1) (First t2) ('[] :: [Symbol])))) a
+ PEG.Syntax: [Seq] :: forall s (env :: Env) (t1 :: Ty) a1 a (t2 :: Ty). PExp s env t1 (a1 -> a) -> PExp s env t2 a1 -> PExp s env ('MkTy (And (Nullable t1) (Nullable t2)) (Union (First t1) (If (Nullable t1) (First t2) ('[] :: [Symbol])))) a
- PEG.Syntax: [Star] :: forall (env :: Env) (f :: [Symbol]) a1. PExp env ('MkTy 'False f) a1 -> PExp env ('MkTy 'True f) [a1]
+ PEG.Syntax: [Star] :: forall s (env :: Env) (f :: [Symbol]) a1. PExp s env ('MkTy 'False f) a1 -> PExp s env ('MkTy 'True f) [a1]
- PEG.Syntax: [Term] :: forall (env :: Env). Char -> PExp env ('MkTy 'False ('[] :: [Symbol])) Char
+ PEG.Syntax: [Term] :: forall s (env :: Env). Char -> PExp s env ('MkTy 'False ('[] :: [Symbol])) Char
- PEG.Syntax: align :: forall (env :: Env) (ty :: Ty) a. PExp env ty a -> PExp env ty a
+ PEG.Syntax: align :: forall s (env :: Env) (ty :: Ty) a. PExp s env ty a -> PExp s env ty a
- PEG.Syntax: data Name (s :: Symbol)
+ PEG.Syntax: data Name (n :: Symbol)
- PEG.Syntax: data PExp (env :: Env) (ty :: Ty) a
+ PEG.Syntax: data PExp s (env :: Env) (ty :: Ty) a
- PEG.Syntax: fmapP :: forall a b (env :: Env) (ty :: Ty). (a -> b) -> PExp env ty a -> PExp env ty b
+ PEG.Syntax: fmapP :: forall a b s (env :: Env) (ty :: Ty). (a -> b) -> PExp s env ty a -> PExp s env ty b
- PEG.Syntax: indent :: forall (n :: Symbol) (env :: Env) (ty :: Ty) a. Rel n -> PExp env ty a -> PExp env ty a
+ PEG.Syntax: indent :: forall (n :: Symbol) s (env :: Env) (ty :: Ty) a. Rel n -> PExp s env ty a -> PExp s env ty a
- PEG.Syntax: nt :: forall (s :: Symbol) (env :: Env). (KnownSymbol s, KnownMember s env (TyOf (Lookup s env)) (ResOf (Lookup s env))) => PExp env (NTTy s env) (ResOf (Lookup s env))
+ PEG.Syntax: nt :: forall (n :: Symbol) (env :: Env) s (ty :: Ty) a. (KnownSymbol n, Lookup n env ~ 'EnvEntry ty a, KnownMember n env a) => PExp s env (NTGo n ty) a
- PEG.Syntax: oneOf :: forall (env :: Env). [Char] -> PExp env ('MkTy 'False ('[] :: [Symbol])) Char
+ PEG.Syntax: oneOf :: forall s (env :: Env). [Char] -> PExp s env ('MkTy 'False ('[] :: [Symbol])) Char
- PEG.Syntax: opt :: forall (env :: Env) (t :: Ty) a. PExp env t a -> PExp env (ChoiceTy t ('MkTy 'True ('[] :: [Symbol]))) (Maybe a)
+ PEG.Syntax: opt :: forall s (env :: Env) (t :: Ty) a. PExp s env t a -> PExp s env (ChoiceTy t ('MkTy 'True ('[] :: [Symbol]))) (Maybe a)
- PEG.Syntax: plus :: forall (env :: Env) (f :: [Symbol]) a. PExp env ('MkTy 'False f) a -> PExp env (SeqTy ('MkTy 'False f) ('MkTy 'True f)) [a]
+ PEG.Syntax: plus :: forall s (env :: Env) (f :: [Symbol]) a. PExp s env ('MkTy 'False f) a -> PExp s env (SeqTy ('MkTy 'False f) ('MkTy 'True f)) [a]
- PEG.Syntax: position :: forall (n :: Symbol) (env :: Env) (ty :: Ty) a. Rel n -> PExp env ty a -> PExp env ty a
+ PEG.Syntax: position :: forall (n :: Symbol) s (env :: Env) (ty :: Ty) a. Rel n -> PExp s env ty a -> PExp s env ty a
- PEG.Syntax: pureP :: forall a (env :: Env). a -> PExp env ('MkTy 'True ('[] :: [Symbol])) a
+ PEG.Syntax: pureP :: forall a s (env :: Env). a -> PExp s env ('MkTy 'True ('[] :: [Symbol])) a
- PEG.Syntax: stringNE :: forall (env :: Env). String -> PExp env ('MkTy 'False ('[] :: [Symbol])) String
+ PEG.Syntax: stringNE :: forall s (env :: Env). String -> PExp s env ('MkTy 'False ('[] :: [Symbol])) String
- PEG.Syntax: type NTTy (s :: Symbol) (env :: Env) = 'MkTy Nullable TyOf Lookup s env ConsIfAbsent s First TyOf Lookup s env
+ PEG.Syntax: type NTTy (n :: Symbol) (env :: Env) = NTGo n TyOf Lookup n env

Files

CHANGELOG.md view
@@ -1,5 +1,229 @@ # Changelog +## Unreleased — compile time of large grammars++Checking a grammar was **exponential in the size of its FIRST sets**.  On a+chain of `n` mutually referring rules, GHC needed 0.7 s at `n = 8`, 12 s at+`n = 12`, and more than five minutes at `n = 15`; anything the size of a real+language front end never finished.  The same grammars now check in+milliseconds-to-seconds and the curve is polynomial: `n = 12` takes 0.5 s,+`n = 30` 1.7 s, `n = 60` 14 s.++One limit is new rather than fixed: the union of two FIRST sets nests one+type-family reduction per element of the result, so a FIRST set of more than+about a hundred non-terminals now reports `Reduction stack overflow` instead+of being slow.  `-freduction-depth=0` lifts it, and a union of two 128-element+sets then takes about 0.3 s.++### Fixed++- **`Union` and `ConsIfAbsent` were exponential.**  `ConsIfAbsent x xs`+  expanded to `If (Elem x xs) xs (x ': xs)`, naming `xs` three times.  In+  `Union (x ': xs) ys = ConsIfAbsent x (Union xs ys)` that `xs` is an+  unreduced `Union`, so each step left GHC three copies of the pending+  computation to reduce and each of those tripled again: `3^n` reductions for+  a union of two `n`-element sets.  Both families now dispatch on an+  already-computed `Ordering` in a helper whose every right-hand side names+  each argument — and in particular the recursive call — exactly once.+- **`Lookup` threaded the whole environment through its recursion** so that+  the not-found case could list the available non-terminals.  An environment+  of `n` rules is `O(n^2)` type nodes, because every entry carries a FIRST+  set, and there is one lookup per occurrence of every non-terminal.  The+  search now carries only the tail it has still to scan; the environment is+  named once, in the branch that reports the error.+- **`Lookup` matched through `CmpSymbol` and a dispatch family**, two+  type-family reductions per entry scanned.  It now matches on a non-linear+  pattern — the name appears twice in the clause — so GHC decides each entry+  by syntactic equality and apartness, in one reduction.  The trick is+  `Data.Type.Map`'s, from `type-level-sets`.  Worth 1.4x-1.6x on a large+  grammar, since the search runs once per occurrence of every non-terminal.+- **`nt` and `PExp`'s `NT` made GHC search the environment several times per+  occurrence.**  Their constraint was+  `KnownMember n env (TyOf (Lookup n env)) (ResOf (Lookup n env))`, and+  resolving `KnownMember` walks `env` one instance at a time, re-normalising+  every index at each step.  Both now name the entry once, through a+  `Lookup n env ~ 'EnvEntry ty a` equality, and pass the resulting rigid+  types to `KnownMember`.++### Changed++- **Breaking.  A FIRST set is now written in alphabetical order**, and a+  declared environment that lists one in any other order is a type error+  naming the first position that disagrees.  Sortedness is what makes a set+  have a single spelling, which is what lets `Union` be one merge pass.+  Migration is mechanical: sort each `'[...]` in your `Env`, so+  `'["term", "factor", "number"]` becomes `'["factor", "number", "term"]`.+- **Breaking.** `Member` and `KnownMember` lose their `Ty` index:+  `Member s env a` and `KnownMember s env a`.  Every index of a class is+  carried along and re-normalised at each step of the instance chain that+  walks the environment, and a `Ty` carries a FIRST set — so an index for it+  made each step cost `O(|env|)`.  Nothing needed it; `Here` binds the+  entry's `ty` existentially, which is enough to pull a rule out of a rule+  table.+- `PEG.Syntax` exports `NTGo`, the `Ty` of a reference to a non-terminal+  whose own `Ty` is already known.  `NTTy n env` is now defined as+  `NTGo n (TyOf (Lookup n env))` and keeps working in signatures.++`SeqTy` and `ChoiceTy` are deliberately **unchanged**.  They duplicate their+operands across their right-hand sides too, but measurement says that costs+nothing here, and writing them as type synonyms is what makes them reduce to a+`'MkTy` head while their operands are still abstract — which is what lets a+polymorphic combinator such as `lexeme` compose without its caller having to+get the nesting of `SeqTy` exactly right.++## 0.2.0.0 — 2026-09-04++This release is **not source-compatible with 0.1.0.0**: `PExp`, `Rules`,+`Grammar`, `Result` and `PState` all gain a leading stream type parameter, and+a rule whose result is a character-class repetition changes result type. See+*Changed* below for the migration.++### Added — parsing any stream, not just `String`++`PEG.Stream` introduces a `Stream` class, with instances for `String`, strict+and lazy `Data.Text.Text`, and strict and lazy `Data.ByteString.ByteString`.+A grammar written once runs over any of them.++The genericity reaches the *results*, not just the input: a character-class+repetition such as `cs:[a-zA-Z0-9_]+` now produces a **chunk of the input+stream** — a real `Text` slice — instead of unpacking into a `[Char]`.  Two new+`PExp` constructors, `Span` and `Span1`, carry this; the quasi-quoter emits+them for `[...]*`, `[...]+`, `'c'*`, `'c'+`, `.*` and `.+`.++`ByteString` is read as Latin-1, exactly as `Data.ByteString.Char8` does: fast,+correct for ASCII, and wrong for multi-byte UTF-8.  `PEG.Stream`'s Haddock+states this as a law rather than a footnote.++Only `unconsS` has no default, so a user instance is one method.  It returns an+unboxed sum rather than `Maybe (Char, s)` on purpose — behind a class+dictionary the boxed version would allocate a `Just` and a pair for every+character, losing the zero-allocation terminal path.++### Changed++- **Breaking.** `PExp`, `Rules` and `Grammar` take a leading stream parameter:+  `PExp s env ty a`, `Rules s env defs`, `Grammar s env ty a`.  `Result` and+  `PState` likewise: `Result s a`, `PState s`.+- **Breaking.** A rule whose result is a character-class repetition now has+  result type `s`, so its `Env` synonym takes a parameter.  Semantic actions+  that fed such a result to something expecting a `String` need+  `chunkToString`: `number <- ds:[0-9]+ { Lit (read (chunkToString ds)) }`.+- **Breaking.** The symbol variable in `PExp`'s `NT`, in `nt`, and in+  `Rules`'s `RCons` is now named `n`; `s` is the stream.  `nt @"name"` is+  unaffected — the name is deliberately still the first quantified variable.+- `PEG.Semantics.Simple`'s unrelated `Stream` class is renamed+  `SimpleStream`, to leave the name to `PEG.Stream`.+- `PState`'s input field is now strict.++A `Grammar` is monomorphic in its stream.  Reusing one across stream types+needs a `forall s. Stream s => Grammar s env ty a` signature, which turns the+value into a function of a dictionary and so stops the compiled parser being+shared between calls.  Give parsers a monomorphic top-level binding where that+matters; `PEG.Parse`'s Haddock spells this out.++### Performance++Measured on the benchmark suite, bytes allocated per input byte, against the+previous release of the evaluator:++| grammar | before (String) | String | Text | ByteString | megaparsec |+|---|---|---|---|---|---|+| arith  | 990 | 943 | 1127 |  969 | 1239 |+| csv    | 834 | 787 |  951 |  805 | 1035 |+| json   | 459 | 404 |  583 |  452 |  782 |+| nested | 265 | 312 |  481 |  336 | 1283 |+| quoted `(!'"' .)*` | 162 | 209 | 320 | 250 | 128 |++`ByteString` is the cheapest column on five of the seven grammars and beats+megaparsec on six.  `Text` costs more than `String` throughout — the same+result the earlier study found for megaparsec, and worth knowing before+reaching for it.++Two grammars regressed on `String` (`nested` +18%, the `(!'"' .)*` idiom++29%).  Both are dominated by single-character steps rather than bulk scans,+where `unconsS` is one indirect call that the previous direct cons-cell match+did not need.  The five grammars that do any bulk scanning improved by 5-12%.++The `idents` and `quoted [^"]*` groups are not in the table because their+grammars changed: `ident` moved from `c:[a-zA-Z_] cs:[a-zA-Z0-9_]*` to+`&[a-zA-Z_] cs:[a-zA-Z0-9_]+` so that it returns a chunk rather than consing a+character onto one, and `Bench.Mega`'s `identP` moved to `takeWhile1P` to keep+the comparison like-for-like.  On the new grammars typed-peg allocates 100+B/byte over `String` and 84 over `ByteString`, against megaparsec's 179.++### Performance++The evaluator was rewritten twice: once around a compilation step, once around+an unboxed step result.  On the benchmark suite in `bench/` (see `cabal bench`),+measured against megaparsec 9.8 in the same run, typed-peg went from taking+2.1x-58x the time megaparsec takes to taking 0.95x-1.17x of it — and it now+allocates less than megaparsec on six of the seven grammars.  The one grammar+where it still loses is the `(!'"' .)*` idiom, which scans every character+twice by construction; written as `[^"]*` it costs 1.23x-1.35x.++- A compiled step returns an unboxed sum, `(# (# #) | (# a, PState #) #)`,+  rather than `Maybe (a, PState)`.  The two are isomorphic, but the unboxed+  sum travels in registers, so a step that succeeds no longer allocates a+  `Just` *and* a pair on top of the new state, and a step that fails+  allocates nothing at all.  This makes `Seq` and `Map` — the two+  constructors the quasi-quoter emits for every grammar item — completely+  allocation-free, and cuts total allocation by a further 9–53%.+- String literals match in a single loop that builds one `PState`, rather+  than one per character, whenever the grammar does not use layout.+- `PEG.Parse` now *compiles* a `Grammar` into a closure once, instead of+  walking the `PExp` GADT and the rule list on every step.  Resolving a+  non-terminal is now one indirect call rather than a linear scan of the rule+  environment.  `parseWith opts g` is written so that partially applying it+  yields the compiled parser; bind it to a name to reuse it.+- Character classes compile to a single `Sat` node holding a `PEG.CharSet`+  (a 256-bit bitmap), instead of expanding into a chain of ordered choices.+  Matching one character of `[a-zA-Z0-9_]` used to cost 63 parser steps.+- String literals compile to a single `Str` node instead of a chain of+  `Seq`/`Map`/`Term`.+- The parser no longer builds a `[(Char, Int)]` copy of the input; the column+  of the current character is carried in the state and updated incrementally.+- Terminals take a fast path that skips all interval arithmetic when the+  ambient column relation is total (`anyR`), which is the case for every+  grammar that does not use layout.  The new `rdTotal` field of `RelD` records+  this.+- `parse` returns the unconsumed suffix in `O(1)` instead of recomputing it+  with two `length` calls and a `drop`.+- `Star` no longer builds a chain of selector thunks.++### Added++- Negated character classes in the quasi-quoter: `[^"]` matches any character+  other than a quote.  Previously the only way to write this was+  `(!'"' .)`, which scans every character twice — once for the lookahead and+  once for the dot.  On the `quoted` benchmark the class form halves the+  allocation.+- `PEG.CharSet`: compact character sets, re-exported from `PEG`.+- `PEG.Syntax.Sat` / `PEG.Syntax.Str` constructors, and the `sat`,+  `charClass` and `notCharClass` smart constructors.+- `PEG.Parse.compileGrammar` and the `Step` and `Res` types, for callers that+  want the compiled parser directly.+- `PEG.Indent.rdTotal`.+- A criterion benchmark suite comparing typed-peg with megaparsec+  (`bench/`, run with `cabal bench`).+- `examples/Compat.hs`: a differential battery used to check that the+  optimisation work did not change any observable behaviour.++### Changed++- **Breaking.** `PState` now holds the remaining input as a `String` plus the+  current column and offset (`stInput`, `stCol`, `stOff`), rather than a+  precomputed `[(Char, Int)]`.  `PEG.Parse.Input`, `PEG.Parse.columns` and+  `PEG.Parse.eval` are gone; use `compileGrammar` instead of `eval`.+- **Breaking.** `RelD` has a new `rdTotal` field.+- **Breaking.** `PEG.Parse.Step` now returns the unboxed sum `Res a` instead+  of `Maybe (a, PState)`.  This only affects code that called+  `compileGrammar` directly; `parse` and `parseWith` are unchanged.+- **Breaking.** In a quasi-quoted grammar, a `^` immediately after `[` now+  negates the class instead of standing for itself; write `[\^]` for a class+  containing a caret.+- The `template-haskell` upper bound now admits the version shipped with+  GHC 9.10 (`< 2.24`).+ ## 0.1.0.0 — 2026-08-28  ### Added
README.md view
@@ -12,7 +12,46 @@ - Compile-time left-recursion detection (type error) - Indentation-sensitive parsing (`PEG.Indent`) - Quasi-quoter for concrete grammar syntax (`PEG.QQ`)+- Parses any `PEG.Stream`: `String`, strict/lazy `Text`, strict/lazy+  `ByteString` +## Input streams++A grammar is written once and runs over any stream:++```haskell+import qualified Data.Text as T++parse arith "1+2*3"              -- Result String Exp+parse arith (T.pack "1+2*3")     -- Result Text   Exp+```++Character classes produce a **chunk of the stream**, not a `[Char]`: matching+`[a-z]+` against a `Text` yields a `Text` slice and copies nothing.  Semantic+actions that want a `String` ask for one:++```haskell+number <- ds:[0-9]+   { Lit (read (chunkToString ds)) }+strlit <- '"' cs:[^"]* '"'   { cs }     -- :: s, no copy+```++Only `unconsS` has no default, so adding a stream is one method.++`ByteString` is read as Latin-1, like `Data.ByteString.Char8`: fast and+correct for ASCII, wrong for multi-byte UTF-8.  Decode to `Text` if that+matters.++A `Grammar` is monomorphic in its stream.  To reuse one across several, give+it a `forall s. Stream s => Grammar s Env _ A` signature — but note that makes+it a function of a dictionary, so the compiled parser is no longer shared+between calls.  Bind a monomorphic parser where that matters:++```haskell+arithString :: String -> Result String Exp+arithString = parse arith+{-# NOINLINE arithString #-}+```+ ## Quick start  ```haskell@@ -22,6 +61,42 @@ -- See examples/Arith.hs for a complete arithmetic expression parser ``` +## Grammar size++The nullability and FIRST set of every rule are computed by GHC while it+type-checks the grammar, so a grammar's size shows up as compile time.  A+FIRST set is a type-level list of non-terminal names kept in **alphabetical+order**:++```haskell+type CalcEnv =+  '[ '("expr" , 'EnvEntry ('MkTy 'False '["atom", "term", "unary"]) Expr)+   , '("term" , 'EnvEntry ('MkTy 'False '["atom", "unary"])         Expr)+   , '("unary", 'EnvEntry ('MkTy 'False '["atom"])                  Expr)+   , '("atom" , 'EnvEntry ('MkTy 'False '[])                        Expr)+   ]+```++The order is not cosmetic.  It gives a set exactly one spelling, which is what+lets the union of two FIRST sets be a single merge pass; listing one in some+other order is a type error naming the first position that disagrees.++That merge nests one type-family reduction per element of the result, so a+grammar with a FIRST set of more than about a hundred non-terminals hits GHC's+default reduction limit and reports `Reduction stack overflow`.  Add+`-freduction-depth=0` to `ghc-options` if you get there; it is a limit rather+than a slowdown, and a union of two 128-element sets takes about 0.3 s once it+is lifted.++## Patterns++[`peg-patterns.md`](peg-patterns.md) works through patterns for specifying+languages with PEGs and this library, following Willis and Wu's *Design+Patterns for Parser Combinators* (Haskell 2021) and noting where a PEG differs+— committed choice, left recursion as a type error, keywords as negative+lookahead — and where typed-peg cannot yet follow.  Every fragment in it+compiles, in [`examples/Patterns.hs`](examples/Patterns.hs).+ ## Building  ```bash@@ -32,6 +107,63 @@  ```bash cabal test typed-peg-examples+```++## Benchmarks++`bench/` holds a criterion suite that measures typed-peg against+[megaparsec](https://hackage.haskell.org/package/megaparsec) on seven grammars+(arithmetic expressions, CSV, identifier lists, a mini JSON, deeply nested+parentheses, and quoted strings spelled two ways) written twice, rule for+rule.  Both libraries consume byte-identical inputs, and the suite+cross-checks that they produce the same result before timing anything.++```bash+cabal bench+```++`cabal bench --benchmark-options=--alloc` prints bytes allocated per parse+instead of running criterion; allocation is the number that separates the two+libraries most clearly once the algorithmic differences are gone.++On GHC 9.10.3 against megaparsec 9.8.1, bytes allocated per input byte on the+largest input of each group:++| grammar | typed-peg `String` | `Text` | `ByteString` | megaparsec `String` |+|---|---|---|---|---|+| arithmetic | 943 | 1127 | 969 | 1239 |+| CSV | 787 | 951 | 805 | 1035 |+| identifiers | 100 | 190 | 84 | 179 |+| JSON | 404 | 583 | 452 | 782 |+| nested parens | 312 | 481 | 336 | 1283 |+| `'"' [^"]* '"'` | 90 | 167 | 65 | 128 |+| `'"' (!'"' .)* '"'` | 209 | 320 | 250 | 128 |++`ByteString` is the cheapest column on five of the seven grammars and beats+megaparsec on six.  `Text` costs more than `String` throughout — the same+result the study found for megaparsec, so reach for it for interoperability+rather than for speed.++Allocation is deterministic and reproduces exactly.  Time is the noisier+measurement: on a machine with heterogeneous cores, unpinned runs of the+*same* megaparsec binary varied by up to 1.8x, so only the ratio taken within+one run is meaningful.++The reference implementation is `Bench.Peg`; its megaparsec twin is+`Bench.Mega`.  Since PEG ordered choice backtracks unconditionally while+megaparsec's `<|>` does not, every megaparsec alternative that can consume+input before failing is wrapped in `try`, so the two are recognising the same+language.++### Parsing many inputs++`parseWith opts grammar` traverses the grammar and returns a compiled closure.+Bind it once and reuse it, rather than calling `parse grammar input` inline in+a loop:++```haskell+myParser :: String -> Result Exp+myParser = parse myGrammar ```  ## License
+ bench/Bench/Inputs.hs view
@@ -0,0 +1,92 @@+-- | Deterministic input generators shared by the typed-peg and megaparsec+-- benchmark groups.  Everything is pure and reproducible (a small LCG), so+-- both libraries are always measured on byte-identical inputs.+module Bench.Inputs+  ( arithInput+  , csvInput+  , identInput+  , jsonInput+  , nestedInput+  , quotedInput+  ) where++-- | A tiny linear congruential generator (glibc constants) so the benchmark+-- inputs do not depend on @random@.+lcg :: Int -> Int+lcg s = (1103515245 * s + 12345) `mod` 2147483648++randoms :: Int -> [Int]+randoms = drop 1 . iterate lcg++-- | @arithInput n@ builds an arithmetic expression with @n@ operands, mixing+-- binary operators, parentheses and unary minus.+arithInput :: Int -> String+arithInput n = go n (randoms 7)+  where+    go k rs+      | k <= 1    = operand rs+      | otherwise = case drop 2 rs of+          (r : rs') -> operand rs ++ ["+-*/" !! (r `mod` 4)] ++ go (k - 1) rs'+          []        -> operand rs++    operand (r : s : _) = case r `mod` 8 of+      0 -> "(" ++ show (s `mod` 1000) ++ "+" ++ show (s `mod` 97) ++ ")"+      1 -> "-" ++ show (s `mod` 1000)+      _ -> show (s `mod` 100000)+    operand _ = "0"++-- | @csvInput rows cols@ builds @rows@ lines of @cols@ comma-separated+-- integers.+csvInput :: Int -> Int -> String+csvInput rows cols =+  intercalate' "\n"+    [ intercalate' "," [ show (v `mod` 1000000) | v <- take cols (drop (r * cols) vs) ]+    | r <- [0 .. rows - 1]+    ]+  where+    vs = randoms 42++-- | @identInput n@ builds @n@ space-separated identifiers.  Identifiers use a+-- wide character class (@[a-zA-Z0-9_]@), which is the worst case for a parser+-- that expands classes into a chain of ordered choices.+identInput :: Int -> String+identInput n = unwords' [ ident v | v <- take n (randoms 3) ]+  where+    alphabet = ['a' .. 'z'] ++ ['A' .. 'Z'] ++ ['0' .. '9'] ++ "_"+    ident v  = 'z' : [ alphabet !! ((v `div` (7 ^ k)) `mod` length alphabet)+                     | k <- [1 .. 6 :: Int] ]++-- | @jsonInput n@ builds a JSON array of @n@ small objects.+jsonInput :: Int -> String+jsonInput n =+  "[" ++ intercalate' ",\n " [ obj v | v <- take n (randoms 11) ] ++ "]"+  where+    obj v = "{\"id\": " ++ show (v `mod` 100000)+         ++ ", \"name\": \"item" ++ show (v `mod` 997) ++ "\""+         ++ ", \"tags\": [" ++ intercalate' ", " [ show (t :: Int) | t <- [1 .. 3] ] ++ "]"+         ++ ", \"ok\": " ++ (if even v then "true" else "false")+         ++ ", \"extra\": null}"++-- | @quotedInput n@ builds @n@ space-separated double-quoted strings.  Used to+-- compare the two ways of spelling \"any character but a quote\": the PEG+-- idiom @(!'\"' .)*@, which scans every character twice, against the negated+-- character class @[^\"]*@.+quotedInput :: Int -> String+quotedInput n = unwords' [ "\"" ++ body v ++ "\"" | v <- take n (randoms 23) ]+  where+    body v = [ alphabet !! ((v `div` (5 ^ k)) `mod` length alphabet)+             | k <- [1 .. 12 :: Int] ]+    alphabet = ['a' .. 'z'] ++ ['A' .. 'Z'] ++ " ,.;:!?-"++-- | @nestedInput d@ builds @d@ nested parentheses around a literal.  This is+-- the deep-recursion / backtracking stress case for the arithmetic grammar.+nestedInput :: Int -> String+nestedInput d = replicate d '(' ++ "1" ++ replicate d ')'++intercalate' :: String -> [String] -> String+intercalate' _   []       = []+intercalate' _   [x]      = x+intercalate' sep (x : xs) = x ++ sep ++ intercalate' sep xs++unwords' :: [String] -> String+unwords' = intercalate' " "
+ bench/Bench/Mega.hs view
@@ -0,0 +1,191 @@+{-# LANGUAGE ConstraintKinds     #-}+{-# LANGUAGE FlexibleContexts    #-}+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies        #-}++-- | The megaparsec side of the benchmark suite.+--+-- The grammars mirror "Bench.Peg" rule-for-rule.  Because PEG ordered choice+-- backtracks unconditionally while megaparsec's '<|>' only backtracks when the+-- left branch consumed nothing, every alternative that can consume input+-- before failing is wrapped in 'try'.  Without that the two libraries would+-- not be recognising the same language.+--+-- Parsers are polymorphic in the stream so the same code can be measured over+-- 'String' (the input type typed-peg supports) and over 'Data.Text.Text' (what+-- a megaparsec user would actually reach for).+module Bench.Mega+  ( runArith+  , runCsv+  , runIdents+  , runJson+  , runQuoted+  ) where++import Data.Char             (isAlphaNum, isAlpha, isDigit)+import Data.Proxy            (Proxy (..))+import Data.String           (IsString)+import Data.Void             (Void)+import Text.Megaparsec+import Text.Megaparsec.Char  (char, string)++import Bench.Peg (Exp (..), JValue (..), evalExp)++type Str s = ( Stream s, VisualStream s, TraversableStream s+              , Token s ~ Char, IsString (Tokens s), Ord (Token s) )++type P s = Parsec Void s++--------------------------------------------------------------------------------+-- Arithmetic expressions+--------------------------------------------------------------------------------++addOp :: Exp -> (Char, Exp) -> Exp+addOp l ('+', r) = Add l r+addOp l ('-', r) = Sub l r+addOp l ('*', r) = Mul l r+addOp l ('/', r) = Div l r+addOp _ (c  , _) = error ("addOp: unexpected operator " ++ show c)++exprP :: Str s => P s Exp+exprP = foldl addOp <$> termP <*> many (try ((,) <$> satisfy addSym <*> termP))+  where addSym c = c == '+' || c == '-'++termP :: Str s => P s Exp+termP = foldl addOp <$> factorP <*> many (try ((,) <$> satisfy mulSym <*> factorP))+  where mulSym c = c == '*' || c == '/'++factorP :: Str s => P s Exp+factorP =+      try numberP+  <|> try (char '(' *> exprP <* char ')')+  <|> (Neg <$> (char '-' *> factorP))++numberP :: Str s => P s Exp+numberP = (Lit . read) <$> some (satisfy isDigit)++--------------------------------------------------------------------------------+-- CSV of integers+--------------------------------------------------------------------------------++csvP :: Str s => P s [[Int]]+csvP = (:) <$> rowP <*> many (try (char '\n' *> rowP))++rowP :: Str s => P s [Int]+rowP = (:) <$> natP <*> many (try (char ',' *> natP))++natP :: Str s => P s Int+natP = read <$> some (satisfy isDigit)++--------------------------------------------------------------------------------+-- Identifier list+--------------------------------------------------------------------------------++identsP :: Str s => P s [Tokens s]+identsP = (:) <$> identP <*> many (try (char ' ' *> identP))++-- Both sides use their bulk primitive: typed-peg spans the class into a chunk+-- of the stream, megaparsec into a 'Tokens'.  A lookahead pins the first+-- character to the narrower class without consuming it, exactly as+-- @&[a-zA-Z_] [a-zA-Z0-9_]+@ does on the typed-peg side.+identP :: Str s => P s (Tokens s)+identP = lookAhead (satisfy startC) *> takeWhile1P Nothing contC+  where+    startC c = isAlpha c || c == '_'+    contC  c = isAlphaNum c || c == '_'++--------------------------------------------------------------------------------+-- Mini JSON+--------------------------------------------------------------------------------++wsP :: Str s => P s ()+wsP = () <$ takeWhileP Nothing isSpace'+  where isSpace' c = c == ' ' || c == '\t' || c == '\r' || c == '\n'++jsonP :: Str s => P s JValue+jsonP = wsP *> valueP <* wsP++valueP :: Str s => P s JValue+valueP =+      try objectP+  <|> try arrayP+  <|> try (JStr <$> strP)+  <|> try numberJP+  <|> try (JBool True  <$ string "true")+  <|> try (JBool False <$ string "false")+  <|> (JNull <$ string "null")++objectP :: Str s => P s JValue+objectP =+  JObj . orEmpty+    <$> (char '{' *> wsP *> optional (try membersP) <* wsP <* char '}')++membersP :: Str s => P s [(String, JValue)]+membersP = (:) <$> pairP <*> many (try (wsP *> char ',' *> wsP *> pairP))++pairP :: Str s => P s (String, JValue)+pairP = (,) <$> strP <*> (wsP *> char ':' *> wsP *> valueP)++arrayP :: Str s => P s JValue+arrayP =+  JArr . orEmpty+    <$> (char '[' *> wsP *> optional (try elemsP) <* wsP <* char ']')++elemsP :: Str s => P s [JValue]+elemsP = (:) <$> valueP <*> many (try (wsP *> char ',' *> wsP *> valueP))++strP :: Str s => P s String+strP = char '"' *> many (satisfy (/= '"')) <* char '"'++numberJP :: Str s => P s JValue+numberJP = mk <$> optional (char '-') <*> some (satisfy isDigit)+  where+    mk Nothing  ds = JNum (read ds)+    mk (Just _) ds = JNum (negate (read ds))++orEmpty :: Maybe [a] -> [a]+orEmpty Nothing   = []+orEmpty (Just xs) = xs++--------------------------------------------------------------------------------+-- Quoted strings+--------------------------------------------------------------------------------++quotedP :: Str s => P s [String]+quotedP = (:) <$> qP <*> many (try (char ' ' *> qP))++qP :: Str s => P s String+qP = char '"' *> many (satisfy (/= '"')) <* char '"'++--------------------------------------------------------------------------------+-- Runners+--------------------------------------------------------------------------------++run :: Str s => P s a -> (a -> Int) -> String -> s -> Int+run p k what s = case runParser p "<bench>" s of+  Left  e -> error (what ++ ": " ++ errorBundlePretty e)+  Right a -> k a++runArith :: Str s => s -> Int+runArith = run exprP evalExp "runArith"++runCsv :: Str s => s -> Int+runCsv = run csvP (sum . map sum) "runCsv"++runIdents :: forall s. Str s => s -> Int+runIdents = run identsP (sum . map (chunkLength (Proxy :: Proxy s))) "runIdents"++runJson :: Str s => s -> Int+runJson = run jsonP sizeJ "runJson"++runQuoted :: Str s => s -> Int+runQuoted = run quotedP (sum . map length) "runQuoted"++sizeJ :: JValue -> Int+sizeJ JNull     = 1+sizeJ (JBool _) = 1+sizeJ (JNum n)  = n+sizeJ (JStr t)  = length t+sizeJ (JArr xs) = 1 + sum (map sizeJ xs)+sizeJ (JObj ps) = 1 + sum [ length k + sizeJ v | (k, v) <- ps ]
+ bench/Bench/Peg.hs view
@@ -0,0 +1,416 @@+{-# LANGUAGE DataKinds             #-}+{-# LANGUAGE QuasiQuotes           #-}+{-# LANGUAGE TypeApplications      #-}+{-# LANGUAGE TypeOperators         #-}+{-# LANGUAGE PartialTypeSignatures #-}+{-# OPTIONS_GHC -Wno-partial-type-signatures #-}+{-# OPTIONS_GHC -Wno-missing-signatures #-}++-- | The typed-peg side of the benchmark suite.  Every grammar here has a+-- structurally identical megaparsec counterpart in "Bench.Mega".+module Bench.Peg+  ( Exp (..)+  , evalExp+  , arith+  , csv+  , idents+  , JValue (..)+  , json+  , arithS, csvS, identsS, jsonS, quotedNotS, quotedClsS+  , arithT, csvT, identsT, jsonT, quotedNotT, quotedClsT+  , arithB, csvB, identsB, jsonB, quotedNotB, quotedClsB+  ) where++import qualified Data.ByteString as B+import qualified Data.Text       as T++import PEG+import PEG.QQ (pegRules)++--------------------------------------------------------------------------------+-- Arithmetic expressions+--------------------------------------------------------------------------------++data Exp+  = Lit Int+  | Neg Exp+  | Add Exp Exp+  | Sub Exp Exp+  | Mul Exp Exp+  | Div Exp Exp+  deriving (Eq, Show)++evalExp :: Exp -> Int+evalExp (Lit n)   = n+evalExp (Neg e)   = negate (evalExp e)+evalExp (Add a b) = evalExp a + evalExp b+evalExp (Sub a b) = evalExp a - evalExp b+evalExp (Mul a b) = evalExp a * evalExp b+evalExp (Div a b) = let d = evalExp b in if d == 0 then 0 else evalExp a `div` d++addOp :: Exp -> (Char, Exp) -> Exp+addOp l ('+', r) = Add l r+addOp l ('-', r) = Sub l r+addOp l ('*', r) = Mul l r+addOp l ('/', r) = Div l r+addOp _ (c  , _) = error ("addOp: unexpected operator " ++ show c)++foldOps :: Exp -> [(Char, Exp)] -> Exp+foldOps = foldl addOp++readInt :: Stream s => s -> Exp+readInt ds = Lit (read (chunkToString ds))++type ArithEnv =+  '[ '("expr"  , 'EnvEntry ('MkTy 'False '["factor", "number", "term"]) Exp)+   , '("term"  , 'EnvEntry ('MkTy 'False '["factor", "number"])         Exp)+   , '("factor", 'EnvEntry ('MkTy 'False '["number"])                   Exp)+   , '("number", 'EnvEntry ('MkTy 'False '[])                           Exp)+   ]++{-# INLINABLE arith #-}+arith :: Stream s => Grammar s ArithEnv _ Exp+arith =+  Grammar+    [pegRules|+       expr   <- t:term ts:(o:[+-] u:term)*   { foldOps t ts }+       term   <- f:factor fs:(o:[*/] g:factor)* { foldOps f fs }+       factor <- n:number+               / '(' e:expr ')'+               / '-' f:factor                 { Neg f }+       number <- ds:[0-9]+                    { readInt ds }+    |]+    (nt @"expr")++--------------------------------------------------------------------------------+-- CSV of integers+--------------------------------------------------------------------------------++type CsvEnv =+  '[ '("csv", 'EnvEntry ('MkTy 'False '["num", "row"]) [[Int]])+   , '("row", 'EnvEntry ('MkTy 'False '["num"])        [Int])+   , '("num", 'EnvEntry ('MkTy 'False '[])             Int)+   ]++{-# INLINABLE csv #-}+csv :: Stream s => Grammar s CsvEnv _ [[Int]]+csv =+  Grammar+    [pegRules|+       csv <- r:row rs:('\n' t:row)* { r : rs }+       row <- n:num ns:(',' m:num)*  { n : ns }+       num <- ds:[0-9]+              { readNat ds }+    |]+    (nt @"csv")++readNat :: Stream s => s -> Int+readNat = read . chunkToString++--------------------------------------------------------------------------------+-- Identifier list (wide character classes)+--------------------------------------------------------------------------------++-- The environment is parameterised by the stream: @ident@ is a character+-- class, so its result is a chunk of the input.+type IdentEnv s =+  '[ '("idents", 'EnvEntry ('MkTy 'False '["ident"]) [s])+   , '("ident" , 'EnvEntry ('MkTy 'False '[])        s)+   ]++{-# INLINABLE idents #-}+idents :: Stream s => Grammar s (IdentEnv s) _ [s]+idents =+  Grammar+    [pegRules|+       idents <- i:ident is:(' ' j:ident)*     { i : is }+       ident  <- &[a-zA-Z_] cs:[a-zA-Z0-9_]+   { cs }+    |]+    (nt @"idents")++--------------------------------------------------------------------------------+-- Mini JSON+--------------------------------------------------------------------------------++data JValue+  = JNull+  | JBool Bool+  | JNum  Int+  | JStr  String+  | JArr  [JValue]+  | JObj  [(String, JValue)]+  deriving (Eq, Show)++mkNum :: Stream s => Maybe Char -> s -> JValue+mkNum Nothing  ds = JNum (read (chunkToString ds))+mkNum (Just _) ds = JNum (negate (read (chunkToString ds)))++orEmpty :: Maybe [a] -> [a]+orEmpty Nothing   = []+orEmpty (Just xs) = xs++type JsonEnv =+  '[ '("json"   , 'EnvEntry ('MkTy 'False '["array","number","object","strlit","value","ws"]) JValue)+   , '("value"  , 'EnvEntry ('MkTy 'False '["array","number","object","strlit"])              JValue)+   , '("object" , 'EnvEntry ('MkTy 'False '[])                                                JValue)+   , '("members", 'EnvEntry ('MkTy 'False '["pair","strlit"])                    [(String, JValue)])+   , '("pair"   , 'EnvEntry ('MkTy 'False '["strlit"])                             (String, JValue))+   , '("array"  , 'EnvEntry ('MkTy 'False '[])                                                JValue)+   , '("elems"  , 'EnvEntry ('MkTy 'False '["array","number","object","strlit","value"])    [JValue])+   , '("strlit" , 'EnvEntry ('MkTy 'False '[])                                                String)+   , '("number" , 'EnvEntry ('MkTy 'False '[])                                                JValue)+   , '("ws"     , 'EnvEntry ('MkTy 'True  '[])                                                    ())+   ]++{-# INLINABLE json #-}+json :: Stream s => Grammar s JsonEnv _ JValue+json =+  Grammar+    [pegRules|+       json    <- ws v:value ws                    { v }+       value   <- o:object                         { o }+                / a:array                          { a }+                / s:strlit                         { JStr s }+                / n:number                         { n }+                / "true"                           { JBool True }+                / "false"                          { JBool False }+                / "null"                           { JNull }+       object  <- '{' ws ms:members? ws '}'        { JObj (orEmpty ms) }+       members <- p:pair ps:(ws ',' ws q:pair)*    { p : ps }+       pair    <- k:strlit ws ':' ws v:value       { (k, v) }+       array   <- '[' ws es:elems? ws ']'          { JArr (orEmpty es) }+       elems   <- e:value es:(ws ',' ws f:value)*  { e : es }+       strlit  <- '"' cs:(!'"' c:.)* '"'           { cs }+       number  <- s:'-'? ds:[0-9]+                 { mkNum s ds }+       ws      <- [ \t\r\n]*+    |]+    (nt @"json")++--------------------------------------------------------------------------------+-- Quoted strings: negative lookahead vs. negated character class+--+-- Two grammars that accept exactly the same language.  The first spells+-- \"any character but a quote\" the way a PEG traditionally does, with a+-- negative lookahead; the second uses a negated character class, which+-- compiles to one 'Sat' node.+--------------------------------------------------------------------------------++-- @(!'"' .)*@ is a compound repetition, so it still yields a @['Char']@ ...+type QuotedNotEnv =+  '[ '("qs", 'EnvEntry ('MkTy 'False '["q"]) [String])+   , '("q" , 'EnvEntry ('MkTy 'False '[])    String)+   ]++-- ... whereas @[^"]*@ is a character class and yields a chunk.+type QuotedClsEnv s =+  '[ '("qs", 'EnvEntry ('MkTy 'False '["q"]) [s])+   , '("q" , 'EnvEntry ('MkTy 'False '[])    s)+   ]++{-# INLINABLE quotedNot #-}+quotedNot :: Stream s => Grammar s QuotedNotEnv _ [String]+quotedNot =+  Grammar+    [pegRules|+       qs <- s:q ss:(' ' t:q)*      { s : ss }+       q  <- '"' cs:(!'"' c:.)* '"' { cs }+    |]+    (nt @"qs")++{-# INLINABLE quotedCls #-}+quotedCls :: Stream s => Grammar s (QuotedClsEnv s) _ [s]+quotedCls =+  Grammar+    [pegRules|+       qs <- s:q ss:(' ' t:q)*      { s : ss }+       q  <- '"' cs:[^"]* '"'       { cs }+    |]+    (nt @"qs")++--------------------------------------------------------------------------------+-- Runners (force the result so criterion measures the whole parse)+--+-- Each parser is bound monomorphically at each stream type.  That matters: a+-- grammar left polymorphic in its stream is a function of a 'Stream'+-- dictionary rather than a constant, so the compiled parser would be rebuilt+-- on every call.  NOINLINE keeps each one a shared CAF, so the measurement is+-- of parsing rather than of re-traversing the grammar.+--------------------------------------------------------------------------------++runArith :: Stream s => (s -> Result s Exp) -> s -> Int+runArith p s = case p s of+  OK e _ _ -> evalExp e+  Fail     -> error "runArith: parse failed"+{-# INLINE runArith #-}++runCsv :: Stream s => (s -> Result s [[Int]]) -> s -> Int+runCsv p s = case p s of+  OK rs _ _ -> sum (map sum rs)+  Fail      -> error "runCsv: parse failed"+{-# INLINE runCsv #-}++runIdents :: Stream s => (s -> Result s [s]) -> s -> Int+runIdents p s = case p s of+  OK is _ _ -> sum (map lengthS is)+  Fail      -> error "runIdents: parse failed"+{-# INLINE runIdents #-}++runJson :: Stream s => (s -> Result s JValue) -> s -> Int+runJson p s = case p s of+  OK v _ _ -> sizeJ v+  Fail     -> error "runJson: parse failed"+{-# INLINE runJson #-}++runQuotedNot :: Stream s => (s -> Result s [String]) -> s -> Int+runQuotedNot p s = case p s of+  OK xs _ _ -> sum (map length xs)+  Fail      -> error "runQuotedNot: parse failed"+{-# INLINE runQuotedNot #-}++runQuotedCls :: Stream s => (s -> Result s [s]) -> s -> Int+runQuotedCls p s = case p s of+  OK xs _ _ -> sum (map lengthS xs)+  Fail      -> error "runQuotedCls: parse failed"+{-# INLINE runQuotedCls #-}++sizeJ :: JValue -> Int+sizeJ JNull      = 1+sizeJ (JBool _)  = 1+sizeJ (JNum n)   = n+sizeJ (JStr t)   = length t+sizeJ (JArr xs)  = 1 + sum (map sizeJ xs)+sizeJ (JObj ps)  = 1 + sum [ length k + sizeJ v | (k, v) <- ps ]++--------------------------------------------------------------------------------+-- Monomorphic entry points, one set per stream.+--+-- The parser must be bound as its own CAF.  Writing @arithS = runArith (parse+-- arith)@ instead lets GHC eta-expand to @\s -> case parse arith s of ...@,+-- which rebuilds the compiled parser on every single call -- a 2.5x slowdown+-- that no amount of specialisation recovers.+--------------------------------------------------------------------------------++pArithS :: String -> Result String Exp+pArithS = parse arith+{-# NOINLINE pArithS #-}++arithS :: String -> Int+arithS = runArith pArithS++pCsvS :: String -> Result String [[Int]]+pCsvS = parse csv+{-# NOINLINE pCsvS #-}++csvS :: String -> Int+csvS = runCsv pCsvS++pJsonS :: String -> Result String JValue+pJsonS = parse json+{-# NOINLINE pJsonS #-}++jsonS :: String -> Int+jsonS = runJson pJsonS++pQuotedNotS :: String -> Result String [String]+pQuotedNotS = parse quotedNot+{-# NOINLINE pQuotedNotS #-}++quotedNotS :: String -> Int+quotedNotS = runQuotedNot pQuotedNotS++pIdentsS :: String -> Result String [String]+pIdentsS = parse idents+{-# NOINLINE pIdentsS #-}++identsS :: String -> Int+identsS = runIdents pIdentsS++pQuotedClsS :: String -> Result String [String]+pQuotedClsS = parse quotedCls+{-# NOINLINE pQuotedClsS #-}++quotedClsS :: String -> Int+quotedClsS = runQuotedCls pQuotedClsS++pArithT :: T.Text -> Result T.Text Exp+pArithT = parse arith+{-# NOINLINE pArithT #-}++arithT :: T.Text -> Int+arithT = runArith pArithT++pCsvT :: T.Text -> Result T.Text [[Int]]+pCsvT = parse csv+{-# NOINLINE pCsvT #-}++csvT :: T.Text -> Int+csvT = runCsv pCsvT++pJsonT :: T.Text -> Result T.Text JValue+pJsonT = parse json+{-# NOINLINE pJsonT #-}++jsonT :: T.Text -> Int+jsonT = runJson pJsonT++pQuotedNotT :: T.Text -> Result T.Text [String]+pQuotedNotT = parse quotedNot+{-# NOINLINE pQuotedNotT #-}++quotedNotT :: T.Text -> Int+quotedNotT = runQuotedNot pQuotedNotT++pIdentsT :: T.Text -> Result T.Text [T.Text]+pIdentsT = parse idents+{-# NOINLINE pIdentsT #-}++identsT :: T.Text -> Int+identsT = runIdents pIdentsT++pQuotedClsT :: T.Text -> Result T.Text [T.Text]+pQuotedClsT = parse quotedCls+{-# NOINLINE pQuotedClsT #-}++quotedClsT :: T.Text -> Int+quotedClsT = runQuotedCls pQuotedClsT++pArithB :: B.ByteString -> Result B.ByteString Exp+pArithB = parse arith+{-# NOINLINE pArithB #-}++arithB :: B.ByteString -> Int+arithB = runArith pArithB++pCsvB :: B.ByteString -> Result B.ByteString [[Int]]+pCsvB = parse csv+{-# NOINLINE pCsvB #-}++csvB :: B.ByteString -> Int+csvB = runCsv pCsvB++pJsonB :: B.ByteString -> Result B.ByteString JValue+pJsonB = parse json+{-# NOINLINE pJsonB #-}++jsonB :: B.ByteString -> Int+jsonB = runJson pJsonB++pQuotedNotB :: B.ByteString -> Result B.ByteString [String]+pQuotedNotB = parse quotedNot+{-# NOINLINE pQuotedNotB #-}++quotedNotB :: B.ByteString -> Int+quotedNotB = runQuotedNot pQuotedNotB++pIdentsB :: B.ByteString -> Result B.ByteString [B.ByteString]+pIdentsB = parse idents+{-# NOINLINE pIdentsB #-}++identsB :: B.ByteString -> Int+identsB = runIdents pIdentsB++pQuotedClsB :: B.ByteString -> Result B.ByteString [B.ByteString]+pQuotedClsB = parse quotedCls+{-# NOINLINE pQuotedClsB #-}++quotedClsB :: B.ByteString -> Int+quotedClsB = runQuotedCls pQuotedClsB
+ bench/Main.hs view
@@ -0,0 +1,165 @@+{-# LANGUAGE OverloadedStrings #-}++-- | criterion driver comparing typed-peg against megaparsec.+--+-- Each grammar is written twice, rule for rule (see "Bench.Peg" and+-- "Bench.Mega"), and both libraries consume the exact same input.+--+-- typed-peg is measured over 'String', 'Data.Text.Text' and+-- 'Data.ByteString.ByteString'; megaparsec over 'String' and+-- 'Data.Text.Text' only, because its @Token ByteString@ is 'Data.Word.Word8'+-- rather than 'Char', so the same grammars do not typecheck over it.+module Main (main) where++import Control.DeepSeq   (force)+import Control.Exception (evaluate)+import Criterion.Main+import qualified Data.ByteString.Char8 as BC+import qualified Data.ByteString       as B+import qualified Data.Text             as T+import GHC.Stats          (RTSStats (..), getRTSStats)+import System.Environment (getArgs)+import System.Mem         (performGC)++import qualified Bench.Inputs as I+import qualified Bench.Mega   as M+import qualified Bench.Peg    as P++-- | Everything needed to measure one grammar on every library and stream.+data Group = Group+  { gName  :: String+  , gPegS  :: String        -> Int+  , gPegT  :: T.Text        -> Int+  , gPegB  :: B.ByteString  -> Int+  , gMegaS :: String        -> Int+  , gMegaT :: T.Text        -> Int+  }++groups :: [(Group, [String])]+groups =+  [ ( Group "arith"  P.arithS  P.arithT  P.arithB  M.runArith  M.runArith+    , map I.arithInput [50, 200, 800] )+  , ( Group "csv"    P.csvS    P.csvT    P.csvB    M.runCsv    M.runCsv+    , map (`I.csvInput` 8) [20, 100, 400] )+  , ( Group "idents" P.identsS P.identsT P.identsB M.runIdents M.runIdents+    , map I.identInput [100, 500, 2000] )+  , ( Group "json"   P.jsonS   P.jsonT   P.jsonB   M.runJson   M.runJson+    , map I.jsonInput [10, 50, 200] )+  , ( Group "nested" P.arithS  P.arithT  P.arithB  M.runArith  M.runArith+    , map I.nestedInput [50, 200] )+    -- The same language spelled two ways in typed-peg, against one megaparsec+    -- parser: this isolates the cost of the negative-lookahead idiom.+  , ( Group "quoted-lookahead" P.quotedNotS P.quotedNotT P.quotedNotB+                               M.runQuoted  M.runQuoted+    , map I.quotedInput [50, 200] )+  , ( Group "quoted-class"     P.quotedClsS P.quotedClsT P.quotedClsB+                               M.runQuoted  M.runQuoted+    , map I.quotedInput [50, 200] )+  ]++label :: Group -> String -> String+label g input = gName g ++ " [" ++ show (length input) ++ "B]"++--------------------------------------------------------------------------------+-- Cross-check: every library and every stream must agree before anything is+-- timed, otherwise the measurements compare different amounts of work.+--------------------------------------------------------------------------------++verify :: Group -> String -> IO ()+verify g input = do+  let ps = gPegS  g input+      pt = gPegT  g (T.pack input)+      pb = gPegB  g (BC.pack input)+      ms = gMegaS g input+      mt = gMegaT g (T.pack input)+  if all (== ps) [pt, pb, ms, mt]+    then putStrLn ("  ok  " ++ label g input ++ " -> " ++ show ps)+    else error ("MISMATCH in " ++ label g input+                  ++ ": peg(String)=" ++ show ps+                  ++ " peg(Text)="    ++ show pt+                  ++ " peg(BS)="      ++ show pb+                  ++ " mega(String)=" ++ show ms+                  ++ " mega(Text)="   ++ show mt)++verifyAll :: IO ()+verifyAll = do+  putStrLn "== cross-checking typed-peg against megaparsec, on every stream =="+  sequence_ [ verify g i | (g, is) <- groups, i <- is ]+  putStrLn ""++--------------------------------------------------------------------------------+-- Allocation report+--+-- @cabal bench --benchmark-options=--alloc@ prints bytes allocated per parse+-- instead of running criterion.  Allocation is deterministic, so it is the+-- measurement to trust when the timings are noisy.+--------------------------------------------------------------------------------++allocFor :: (a -> Int) -> a -> IO Integer+allocFor f x = do+  performGC+  before <- getRTSStats+  n <- evaluate (f x)+  n `seq` performGC+  after <- getRTSStats+  pure (fromIntegral (allocated_bytes after - allocated_bytes before))++allocRow :: Group -> String -> IO ()+allocRow g input = do+  s <- evaluate (force input)+  t <- evaluate (force (T.pack input))+  b <- evaluate (force (BC.pack input))+  aps <- allocFor (gPegS  g) s+  apt <- allocFor (gPegT  g) t+  apb <- allocFor (gPegB  g) b+  ams <- allocFor (gMegaS g) s+  amt <- allocFor (gMegaT g) t+  let n = fromIntegral (length input) :: Double+      per v = rjust 9 (showF (fromIntegral v / n))+  putStrLn (concat+    [ pad 24 (label g input)+    , per aps, per apt, per apb, per ams, per amt ])+  where+    pad k x   = x ++ replicate (k - length x) ' '+    rjust k x = replicate (k - length x) ' ' ++ x+    showF v   = show (fromIntegral (round (v * 10) :: Int) / 10 :: Double)++allocReport :: IO ()+allocReport = do+  putStrLn "bytes allocated per input byte"+  putStrLn (concat [ replicate 24 ' '+                   , "  peg/Str", "  peg/Txt", "   peg/BS"+                   , " mega/Str", " mega/Txt" ])+  sequence_ [ allocRow g i | (g, is) <- groups, i <- is ]++--------------------------------------------------------------------------------++main :: IO ()+main = do+  args <- getArgs+  if "--alloc" `elem` args+    then allocReport+    else verifyAll >> defaultMain benchmarks++benchmarks :: [Benchmark]+benchmarks =+  [ bgroup (gName g)+      [ env (prepare input) $ \ ~(s, t, b) ->+          bgroup (label g input)+            [ bench "typed-peg   (String)"     $ whnf (gPegS  g) s+            , bench "typed-peg   (Text)"       $ whnf (gPegT  g) t+            , bench "typed-peg   (ByteString)" $ whnf (gPegB  g) b+            , bench "megaparsec  (String)"     $ whnf (gMegaS g) s+            , bench "megaparsec  (Text)"       $ whnf (gMegaT g) t+            ]+      | input <- is+      ]+  | (g, is) <- groups+  ]++prepare :: String -> IO (String, T.Text, B.ByteString)+prepare s = do+  s' <- evaluate (force s)+  t' <- evaluate (force (T.pack s))+  b' <- evaluate (force (BC.pack s))+  pure (s', t', b')
examples/Arith.hs view
@@ -52,13 +52,19 @@ addOp _ (c  , _) = error ("addOp: unexpected operator " ++ show c)  type ArithEnv =-  '[ '("expr"  , 'EnvEntry ('MkTy 'False '["term", "factor", "number"]) Exp)+  '[ '("expr"  , 'EnvEntry ('MkTy 'False '["factor", "number", "term"]) Exp)    , '("term"  , 'EnvEntry ('MkTy 'False '["factor", "number"])         Exp)    , '("factor", 'EnvEntry ('MkTy 'False '["number"])                   Exp)    , '("number", 'EnvEntry ('MkTy 'False '[])                           Exp)    ] -arith :: Grammar ArithEnv _ Exp+-- | Polymorphic in the stream, so the same grammar can be run over 'String',+-- 'Data.Text.Text' and 'Data.ByteString.ByteString'.  Note the cost: this is+-- a function of a 'Stream' dictionary rather than a constant, so the compiled+-- parser is not shared between calls.  Bind a monomorphic parser+-- (@arithString = parse arith :: String -> Result String Exp@) where that+-- matters.+arith :: Stream s => Grammar s ArithEnv _ Exp arith =   Grammar     [pegRules|@@ -68,6 +74,6 @@        factor <- n:number                / '(' e:expr ')'                / '-' f:factor { Neg f }-       number <- ds:[0-9]+ { Lit (read ds :: Int) }+       number <- ds:[0-9]+ { Lit (read (chunkToString ds) :: Int) }     |]     (nt @"expr")
+ examples/Compat.hs view
@@ -0,0 +1,164 @@+{-# LANGUAGE OverloadedStrings      #-}+{-# LANGUAGE PartialTypeSignatures  #-}+{-# LANGUAGE QuasiQuotes            #-}+{-# LANGUAGE RankNTypes             #-}+{-# LANGUAGE ScopedTypeVariables    #-}+{-# OPTIONS_GHC -Wno-partial-type-signatures #-}++-- | A differential test: it renders the complete 'Result' (value, consumed+-- prefix and remaining suffix) for a fixed battery of inputs.+--+-- It serves two purposes.+--+-- * The output is compared byte-for-byte between successive versions of the+--   evaluator, which is how the optimisation work was checked for behavioural+--   drift — including the layout-sensitive paths that the other examples do+--   not exercise.+--+-- * The same battery is run over 'String', 'Data.Text.Text' and+--   'Data.ByteString.ByteString', and the three renderings must agree.  That+--   is what pins the "PEG.Stream" instances to each other: a stream whose+--   @spanS@ or column bookkeeping is wrong shows up here as a diff.+--+-- The renderings can be compared directly because 'Show' for 'Data.Text.Text'+-- and 'Data.ByteString.ByteString' agrees with 'Show' for 'String' on the+-- Latin-1 range, which is all these inputs use.+module Compat (compatMain) where++import qualified Data.ByteString.Char8 as BC+import qualified Data.Text             as T++import PEG+import PEG.QQ (pegExpr, pegRules)+import Arith  (arith, Exp)+import Layout (DoStmt, doExp, layoutOpts)++showR :: (Show s, Show a) => Result s a -> String+showR (OK a c r) = "OK " ++ show a ++ " consumed=" ++ show c ++ " rest=" ++ show r+showR Fail       = "Fail"++arithCases :: [String]+arithCases =+  [ "1+2*3", "(1+2)*3", "42", "-7", "1+", "", "((((1))))"+  , "1+2)rest", "12*34/5-6", "9"+  , "1+2*3+4*5+6/7-8", "(((1+2)*(3+4))-(5*6))"+  , "0000123", "1--2", "-(1+2)"+  ]++layoutCases :: [String]+layoutCases =+  [ "do\n  foo\n  bar"+  , "do\n  foo\n  bar\nbaz"+  , "do { foo ; bar }"+  , "do\n  foo\n  do\n    bar\n  baz"+  , "do\n foo\n  bar"+  , "do\n\tfoo\n\tbar"+  , "do foo bar"+  , "do"+  , "  do\n    a\n    b"+  , "do\n  a\n b"+  , "do { a }"+  , "do\n  do\n    x"+  ]++-- Several option sets, so tab expansion and the token relation are covered.+optSets :: [(String, Opts)]+optSets =+  [ ("layout(ge,tab8)" , layoutOpts)+  , ("layout(ge,tab4)" , layoutOpts  { optTabWidth  = 4 })+  , ("layout(ge,tab1)" , layoutOpts  { optTabWidth  = 1 })+  , ("layout(gt)"      , layoutOpts  { optTokenMode = relD gtR })+  , ("layout(eq)"      , layoutOpts  { optTokenMode = relD eqR })+  , ("layout(any)"     , defaultOpts)+  , ("layout(off2)"    , layoutOpts  { optTokenMode = relD (offsetR 2) })+  , ("layout(cands)"   , layoutOpts  { optCands     = Interval 1 (Fin 20) })+  ]++-- | Chunk primitives and the lookaheads over them.+--+-- @Span@ and @Span1@ are what a character-class repetition compiles to, and+-- @!c+@ / @!c*@ have dedicated compile cases; nothing else in the battery+-- reaches them.  @!c*@ can never succeed, because the star matches the empty+-- run.+type SpanEnv s =+  '[ '("digits", 'EnvEntry ('MkTy 'True  '[]) s)+   , '("digits1", 'EnvEntry ('MkTy 'False '[]) s)+   ]++spanG :: Stream s => Grammar s (SpanEnv s) _ (s, s)+spanG =+  Grammar+    [pegRules|+       digits  <- ds:[0-9]*   { ds }+       digits1 <- ds:[0-9]+   { ds }+    |]+    [pegExpr| a:digits '/' b:digits1 |]++-- @!'x'+ .@ accepts any character that is not an @x@; @!'x'* .@ accepts+-- nothing at all.+notSpan1G :: Stream s => Grammar s '[] _ Char+notSpan1G = Grammar RNil [pegExpr| !'x'+ c:. |]++notSpanG :: Stream s => Grammar s '[] _ Char+notSpanG = Grammar RNil [pegExpr| !'x'* c:. |]++spanCases :: [String]+spanCases = ["/1", "12/34", "/", "12/", "abc", "", "007/8"]++notCases :: [String]+notCases = ["y", "x", "", "yx"]++-- | The whole battery, rendered as lines, for one stream type.+--+-- @pack@ is the only stream-specific part; everything else is the same code+-- running at a different instance.+battery :: forall s. (Stream s, Show s) => (String -> s) -> [String]+battery pack =+  [ "### arith (defaultOpts)" ]+  ++ [ show s ++ " => " ++ showR (parse arith (pack s) :: Result s Exp)+     | s <- arithCases ]+  ++ [ "### arith (varying Opts)" ]+  ++ [ nm ++ " " ++ show s ++ " => "+         ++ showR (parseWith o arith (pack s) :: Result s Exp)+     | (nm, o) <- optSets, s <- arithCases ]+  ++ [ "### span primitives" ]+  ++ [ show c ++ " => " ++ showR (parse spanG (pack c) :: Result s (s, s))+     | c <- spanCases ]+  ++ [ "### !c+ (peek) and !c* (never succeeds)" ]+  ++ [ show c ++ " => " ++ showR (parse notSpan1G (pack c) :: Result s Char)+         ++ " | " ++ showR (parse notSpanG (pack c) :: Result s Char)+     | c <- notCases ]+  ++ [ "### layout" ]+  ++ [ nm ++ " " ++ show s ++ " => "+         ++ showR (parseWith o doExp (pack s) :: Result s [DoStmt])+     | (nm, o) <- optSets, s <- layoutCases ]++-- | Print the 'String' rendering — this is the output compared against+-- previous versions of the evaluator — then check the other two streams+-- against it.+compatMain :: IO ()+compatMain = do+  let reference = battery id+  mapM_ putStrLn reference++  putStrLn "### stream agreement"+  agree "Data.Text.Text"             reference (battery T.pack)+  agree "Data.ByteString.ByteString" reference (battery BC.pack)++-- | Report the first disagreement, if any.  A count alone would say that+-- something is wrong without saying what, and these are 234 dense lines.+agree :: String -> [String] -> [String] -> IO ()+agree name reference actual =+  case [ (i, r, a)+       | (i, r, a) <- zip3 [1 :: Int ..] reference actual, r /= a ] of+    [] | length reference == length actual ->+           putStrLn (name ++ ": agrees with String on all "+                       ++ show (length reference) ++ " lines")+       | otherwise ->+           putStrLn (name ++ ": MISMATCH in length, " ++ show (length reference)+                       ++ " vs " ++ show (length actual))+    ((i, r, a) : rest) -> do+      putStrLn (name ++ ": MISMATCH on " ++ show (length rest + 1)+                  ++ " line(s), first at line " ++ show i)+      putStrLn ("  String: " ++ r)+      putStrLn ("  " ++ name ++ ": " ++ a)
examples/Layout.hs view
@@ -20,16 +20,20 @@   | Nested [DoStmt]   deriving (Eq, Show) -type DoEnv =+-- | The environment is parameterised by the stream, because @name@ is a+-- character class and so produces a chunk of the input rather than a+-- 'String'.  Any rule whose result is a chunk pushes @s@ into the+-- environment this way.+type DoEnv s =   '[ '("doexp" , 'EnvEntry ('MkTy 'False '[])                               [DoStmt])-   , '("istmts", 'EnvEntry ('MkTy 'False '["ws", "stmt", "doexp", "name"]) [DoStmt])+   , '("istmts", 'EnvEntry ('MkTy 'False '["doexp", "name", "stmt", "ws"]) [DoStmt])    , '("stmts" , 'EnvEntry ('MkTy 'False '["ws"])                          [DoStmt])    , '("stmt"  , 'EnvEntry ('MkTy 'False '["doexp", "name"])               DoStmt)-   , '("name"  , 'EnvEntry ('MkTy 'False '[])                              String)+   , '("name"  , 'EnvEntry ('MkTy 'False '[])                              s)    , '("ws"    , 'EnvEntry ('MkTy 'True  '[])                              ())    ] -doExp :: Grammar DoEnv _ [DoStmt]+doExp :: Stream s => Grammar s (DoEnv s) _ [DoStmt] doExp =   Grammar     [pegRules|@@ -39,7 +43,7 @@         stmts  <- r:(ws '{' ws s:stmt ss:(ws ';' ws t:stmt)* ws '}' { s : ss })^~ -       stmt   <- d:doexp { Nested d } / n:name { Atom n }+       stmt   <- d:doexp { Nested d } / n:name { Atom (chunkToString n) }         name   <- cs:[a-z]+ 
examples/Main.hs view
@@ -3,8 +3,10 @@ import PEG (parse, parseWith, Result(..)) import Arith (arith, evalExp) import Layout (doExp, layoutOpts)+import Compat (compatMain)+import Patterns (patternsMain) -showResult :: Show a => Result a -> String+showResult :: Show a => Result String a -> String showResult (OK a _ _) = "OK " ++ show a showResult Fail        = "Fail" @@ -23,3 +25,9 @@   putStrLn "\n=== Layout (do-notation) ==="   let testLayout s = putStrLn $ showResult (parseWith layoutOpts doExp s)   testLayout "foo\n  bar\n  baz\nqux"++  putStrLn "\n=== Patterns (see peg-patterns.md) ==="+  patternsMain++  putStrLn "\n=== Differential battery ==="+  compatMain
+ examples/Patterns.hs view
@@ -0,0 +1,253 @@+{-# LANGUAGE DataKinds             #-}+{-# LANGUAGE FlexibleContexts      #-}+{-# LANGUAGE QuasiQuotes           #-}+{-# LANGUAGE TypeApplications      #-}+{-# LANGUAGE TypeOperators         #-}+{-# LANGUAGE PartialTypeSignatures #-}+{-# OPTIONS_GHC -Wno-partial-type-signatures #-}++-- | Worked examples for @peg-patterns.md@.+--+-- Every snippet quoted in that document appears here, so the document cannot+-- drift away from code that compiles.  'patternsMain' exercises each one.+module Patterns+  ( Expr (..)+  , Asgn (..)+  , evalE+  , ws, lexeme, keyword, eof, fully+  , calc+  , addOp+  , kwG+  , prog+  , patternsMain+  ) where++import PEG+import PEG.QQ (pegExpr, pegRules)++--------------------------------------------------------------------------------+-- Pattern 2a/2bi: whitespace and token combinators, at the PExp level+--------------------------------------------------------------------------------++-- | Zero or more layout characters.  A character class, so this compiles to a+-- single 'Span' node and returns a chunk of the input.+ws :: PExp s env ('MkTy 'True '[]) s+ws = spanOf (fromRanges [(' ', ' '), ('\t', '\t'), ('\r', '\r'), ('\n', '\n')])++-- | Run @p@, then consume /trailing/ whitespace only.+lexeme :: PExp s env ty a -> PExp s env (SeqTy ty ('MkTy 'True '[])) a+lexeme p = (\x _ -> x) <$>. p <*>. ws++-- | End of input: nothing can follow.+eof :: PExp s env ('MkTy 'True '[]) ()+eof = Not AnyChar++-- | Leading whitespace, then @p@, then end of input.+fully :: PExp s env ty a+      -> PExp s env (SeqTy ('MkTy 'True '[])+                           (SeqTy ty ('MkTy 'True '[]))) a+fully p = (\_ x _ -> x) <$>. ws <*>. p <*>. eof++--------------------------------------------------------------------------------+-- Pattern 2bii: keyword combinator+--------------------------------------------------------------------------------++identCont :: CharSet+identCont = fromRanges [('a', 'z'), ('A', 'Z'), ('0', '9'), ('_', '_')]++-- | Match a keyword that is not a prefix of a longer identifier.+--+-- The negative lookahead is the whole pattern: @keyword "negate"@ fails on+-- @negatex@ because an identifier character follows.  In a backtracking+-- combinator library this needs @try@; in a PEG it is just @!@.+keyword :: String -> PExp s env ('MkTy 'False '[]) ()+keyword k = (\_ _ -> ()) <$>. stringNE k <*>. Not (sat identCont)++--------------------------------------------------------------------------------+-- The AST, one layer per precedence level (Pattern 1b)+--------------------------------------------------------------------------------++data Expr+  = Add Expr Expr+  | Sub Expr Expr+  | Mul Expr Expr+  | Div Expr Expr+  | Neg Expr+  | Num Int+  | Var String+  deriving (Eq, Show)++data Asgn = Asgn String Expr+  deriving (Eq, Show)++evalE :: [(String, Int)] -> Expr -> Int+evalE g (Add a b) = evalE g a + evalE g b+evalE g (Sub a b) = evalE g a - evalE g b+evalE g (Mul a b) = evalE g a * evalE g b+evalE g (Div a b) = let d = evalE g b in if d == 0 then 0 else evalE g a `div` d+evalE g (Neg a)   = negate (evalE g a)+evalE _ (Num n)   = n+evalE g (Var v)   = maybe 0 id (lookup v g)++--------------------------------------------------------------------------------+-- Pattern 3a: lifted constructors+--+-- The semantic actions stay one application wide; the dispatch on which+-- constructor an operator denotes lives in ordinary Haskell.+--------------------------------------------------------------------------------++-- | Fold a left-associative chain: an operand followed by @(op, operand)@+-- pairs.  This is what @chainl1@ buys in a combinator library, written out.+chainl :: Expr -> [(Char, Expr)] -> Expr+chainl = foldl step+  where+    step l ('+', r) = Add l r+    step l ('-', r) = Sub l r+    step l ('*', r) = Mul l r+    step l ('/', r) = Div l r+    step _ (c  , _) = error ("chainl: unexpected operator " ++ show c)++mkNum :: Stream s => s -> Expr+mkNum = Num . read . chunkToString++mkVar :: Stream s => s -> Expr+mkVar = Var . chunkToString++mkAsgn :: Stream s => s -> Expr -> Asgn+mkAsgn v e = Asgn (chunkToString v) e++--------------------------------------------------------------------------------+-- Pattern 1a/1c: a precedence ladder, one rule per level+--------------------------------------------------------------------------------++type CalcEnv s =+  '[ '("expr" , 'EnvEntry ('MkTy 'False '["atom", "term", "unary"]) Expr)+   , '("term" , 'EnvEntry ('MkTy 'False '["atom", "unary"])         Expr)+   , '("unary", 'EnvEntry ('MkTy 'False '["atom"])                  Expr)+   , '("atom" , 'EnvEntry ('MkTy 'False '[])                        Expr)+   ]++-- | The classic expression language.+--+-- Note what is /not/ here: no @try@, no left recursion, and no rule that can+-- loop.  @expr <- expr '+' term@ would be rejected by 'PEG.Grammar.Acyclic'+-- at compile time with a type error naming @expr@.+calc :: Stream s => Grammar s (CalcEnv s) _ Expr+calc =+  Grammar+    [pegRules|+       expr  <- t:term  ts:(o:[+-] u:term)*  { chainl t ts }+       term  <- f:unary fs:(o:[*/] g:unary)* { chainl f fs }+       unary <- '-' e:unary                  { Neg e }+              / a:atom+       atom  <- '(' e:expr ')'+              / ds:[0-9]+                    { mkNum ds }+              / &[a-zA-Z_] cs:[a-zA-Z0-9_]+  { mkVar cs }+    |]+    (nt @"expr")++-- | The same pattern inside a quasi-quoted grammar: a string literal followed+-- by a negative lookahead on the identifier-continuation class.+type KwEnv = '[ '("kw", 'EnvEntry ('MkTy 'False '[]) String) ]++kwG :: Stream s => Grammar s KwEnv _ String+kwG = Grammar [pegRules| kw <- k:"negate" ![a-zA-Z0-9_]  { k } |] (nt @"kw")++--------------------------------------------------------------------------------+-- Pattern 3b: deferred constructors+--+-- A rule may return a *function*, so the choice of constructor is made where+-- the operator is read and applied where the operands are known.  This is the+-- defunctionalised chain the paper describes, and it removes the partial+-- 'error' case from 'chainl' above.+--------------------------------------------------------------------------------++type OpEnv =+  '[ '("op", 'EnvEntry ('MkTy 'False '[]) (Expr -> Expr -> Expr)) ]++addOp :: Stream s => Grammar s OpEnv _ (Expr -> Expr -> Expr)+addOp = Grammar [pegRules| op <- '+' { Add } / '-' { Sub } |] (nt @"op")++--------------------------------------------------------------------------------+-- Statements, to show ordered choice and the lexeme discipline+--------------------------------------------------------------------------------++type ProgEnv s =+  '[ '("prog" , 'EnvEntry ('MkTy 'False '["asgn"]) [Asgn])+   , '("asgn" , 'EnvEntry ('MkTy 'False '[])       Asgn)+   , '("expr" , 'EnvEntry ('MkTy 'False '["atom", "term", "unary"]) Expr)+   , '("term" , 'EnvEntry ('MkTy 'False '["atom", "unary"])         Expr)+   , '("unary", 'EnvEntry ('MkTy 'False '["atom"])                  Expr)+   , '("atom" , 'EnvEntry ('MkTy 'False '[])                        Expr)+   ]++-- | @a := 1; b := a * 2@+--+-- @':='@ comes before @':'@ nowhere in this grammar, but the ordering rule it+-- illustrates is the one PEG newcomers get wrong: in an ordered choice the+-- longer alternative must come first, because the first success wins and+-- there is no backtracking into a committed branch.+prog :: Stream s => Grammar s (ProgEnv s) _ [Asgn]+prog =+  Grammar+    [pegRules|+       prog  <- a:asgn as:(';' b:asgn)*      { a : as }+       asgn  <- &[a-zA-Z_] v:[a-zA-Z0-9_]+ ":=" e:expr  { mkAsgn v e }++       expr  <- t:term  ts:(o:[+-] u:term)*  { chainl t ts }+       term  <- f:unary fs:(o:[*/] g:unary)* { chainl f fs }+       unary <- '-' e:unary                  { Neg e }+              / a:atom+       atom  <- '(' e:expr ')'+              / ds:[0-9]+                    { mkNum ds }+              / &[a-zA-Z_] cs:[a-zA-Z0-9_]+  { mkVar cs }+    |]+    (nt @"prog")++--------------------------------------------------------------------------------+-- Demonstration+--------------------------------------------------------------------------------++showR :: Show a => Result String a -> String+showR (OK a _ r) = "OK " ++ show a ++ (if null r then "" else " rest=" ++ show r)+showR Fail       = "Fail"++patternsMain :: IO ()+patternsMain = do+  putStrLn "### precedence ladder"+  mapM_ (\s -> putStrLn (show s ++ " => " ++ show (fmap' (evalE []) (parse calc s))))+    [ "1+2*3", "(1+2)*3", "2*3+4", "-3+4", "10/2/5", "1-2-3", "x" ]++  putStrLn "### ordered choice / statements"+  mapM_ (\s -> putStrLn (show s ++ " => " ++ showR (parse prog s)))+    [ "a:=1", "a:=1;b:=a*2", "a:=", "a:=1;" ]++  putStrLn "### keyword vs bare literal"+  -- No 'fully' here: the point is what each one leaves behind.+  let kw   = parse (Grammar RNil (keyword "negate"))+               :: String -> Result String ()+      bare = parse (Grammar RNil (const () <$>. stringNE "negate"))+               :: String -> Result String ()+  mapM_ (\s -> putStrLn (show s ++ " keyword => " ++ showR (kw s)+                           ++ " | bare => " ++ showR (bare s)))+    [ "negate", "negatex", "negate2", "negate x" ]++  putStrLn "### keyword, in quasi-quoter syntax"+  mapM_ (\s -> putStrLn (show s ++ " => " ++ showR (parse kwG s)))+    [ "negate", "negatex", "negate x" ]++  putStrLn "### deferred constructor: a rule returning a function"+  mapM_ (\s -> putStrLn (show s ++ " => " +++          case parse addOp s of+            OK f _ _ -> show (f (Num 1) (Num 2))+            Fail     -> "Fail"))+    [ "+", "-", "*" ]++  putStrLn "### lexeme discipline: fully (lexeme p)"+  let toks = parse (Grammar RNil (fully (lexeme [pegExpr| ds:[0-9]+ |])))+               :: String -> Result String String+  mapM_ (\s -> putStrLn (show s ++ " => " ++ showR (toks s)))+    [ "12", "  12  ", "12 x", "" ]+  where+    fmap' f (OK a _ _) = Just (f a)+    fmap' _ Fail       = Nothing
+ peg-patterns.md view
@@ -0,0 +1,573 @@+# PEG patterns for typed-peg++A companion to Jamie Willis and Nicolas Wu, *Design Patterns for Parser+Combinators (Functional Pearl)*, Haskell 2021+([10.1145/3471874.3472984](https://doi.org/10.1145/3471874.3472984)).++That paper collects eleven patterns for writing parsers with a backtracking+combinator library of the `parsec` family. Most of them transfer to typed-peg,+but three things change the picture:++- **Ordered choice is committed.** Once an alternative succeeds, a PEG never+  reconsiders it. There is no `try`, because there is nothing to undo — but the+  order in which you write alternatives becomes part of the specification.+- **Left recursion is a type error**, not a discipline to remember. The+  `Acyclic` constraint is checked when you construct a `Grammar`.+- **The grammar's shape is written down in a type.** The `Env` records every+  rule's nullability, FIRST set and result type. Several of the paper's+  patterns become things the compiler enforces rather than things you adopt.++Every code fragment below is compiled: it lives in+[`examples/Patterns.hs`](examples/Patterns.hs) and runs as part of+`cabal test`.++## The patterns at a glance++| Willis & Wu | In typed-peg |+|---|---|+| 1a Homogeneous Chains | [§1.2](#12-chains-fold-a-starred-tail) — write the fold out; no `chainl1` |+| 1b Heterogeneous Chains | [§1.3](#13-one-rule-per-precedence-level) — one rule per level, types declared in the `Env` |+| 1c Precedence Tables | [§1.4](#14-precedence-tables-absent-but-not-impossible) — absent; what it would take |+| 2a Whitespace Combinators | [§2.1](#21-consume-trailing-whitespace-only) — `lexeme` / `fully`, same discipline |+| 2bi Tokenizing Combinators | [§2.2](#22-tokens) — same |+| 2bii Keyword Combinators | [§2.3](#23-keywords-are-negative-lookahead) — **simpler**: `!` instead of `try` |+| 2c Overloaded Strings | [§2.4](#24-the-quasi-quoter-is-the-facade) — subsumed by the quasi-quoter |+| 3a Lifted Constructors | [§3.1](#31-lifted-constructors) — same |+| 3b Deferred Constructors | [§3.2](#32-deferred-constructors-and-the-position-gap) — **partly**; no source positions |+| 4a Verified Errors | [§4](#4-errors-the-shape-without-the-message) — shape only; no messages |+| 4b Preventative Errors | [§4](#4-errors-the-shape-without-the-message) — shape only; no messages |++Sections [§5](#5-patterns-that-are-specific-to-pegs) and+[§6](#6-what-typed-peg-cannot-do-yet) add patterns the paper has no reason to+cover, and summarise the gaps.++---++## 1. Expressions++### 1.1 Left recursion is a type error++The paper opens by writing the textbook grammar directly:++```haskell+expr = Add <$> expr <*> (char '+' *> term) <|> ... <|> term+```++and observing that it loops. Section 2 is then about the rewrite that fixes it.++In typed-peg you cannot write it in the first place. Each rule's type carries+its FIRST set, and `Grammar` demands `Acyclic env`:++```+expr <- e:expr '+' t:term { Add e t }+```++```+Left-recursive non-terminal: "expr"+Its head set already contains itself: ["expr", "term"]+Violates the acyclicity condition i `notElem` Gamma(i).F.+```++reported at the `Grammar` constructor, before anything runs.++**Pattern.** Do not treat left-recursion removal as a step you perform. Write+the grammar; if it compiles, no rule can loop on its own head. The rewrite+below is then the *only* shape available, which is why it is worth having a+name for.++### 1.2 Chains: fold a starred tail++*(Willis & Wu, Pattern 1a: Homogeneous Chains.)*++Their advice is to reach for `chainl1`/`chainr1` rather than hand-rolling+associativity. typed-peg has no chain combinator, so the pattern is the shape+you write instead: **an operand, then a starred tail of (operator, operand)+pairs, folded in the semantic action.**++```+expr <- t:term  ts:(o:[+-] u:term)*  { chainl t ts }+```++Left association comes from `foldl`, right association from `foldr`. Keep the+fold itself in Haskell, out of the grammar:++```haskell+chainl :: Expr -> [(Char, Expr)] -> Expr+chainl = foldl step+  where+    step l ('+', r) = Add l r+    step l ('-', r) = Sub l r+    step l ('*', r) = Mul l r+    step l ('/', r) = Div l r+    step _ (c  , _) = error ("chainl: unexpected operator " ++ show c)+```++The `error` case is the price of a homogeneous chain: the operator is a `Char`,+so nothing stops a mismatched table. The paper makes exactly this observation,+and its answer is the next pattern.++### 1.3 One rule per precedence level++*(Willis & Wu, Pattern 1b: Heterogeneous Chains.)*++Their fix is to give each precedence level its own AST layer so the types rule+out a mismatched chain. In typed-peg the level structure is *already* forced on+you — a PEG expresses precedence by descent — and the `Env` makes you declare+what each level produces:++```haskell+type CalcEnv s =+  '[ '("expr" , 'EnvEntry ('MkTy 'False '["atom", "term", "unary"]) Expr)+   , '("term" , 'EnvEntry ('MkTy 'False '["atom", "unary"])         Expr)+   , '("unary", 'EnvEntry ('MkTy 'False '["atom"])                  Expr)+   , '("atom" , 'EnvEntry ('MkTy 'False '[])                        Expr)+   ]+```++```+expr  <- t:term  ts:(o:[+-] u:term)*  { chainl t ts }+term  <- f:unary fs:(o:[*/] g:unary)* { chainl f fs }+unary <- '-' e:unary                  { Neg e }+       / a:atom+atom  <- '(' e:expr ')'+       / ds:[0-9]+                    { mkNum ds }+       / &[a-zA-Z_] cs:[a-zA-Z0-9_]+  { mkVar cs }+```++**Pattern.** Give each level a distinct result type in the `Env` when you want+the paper's type safety. Above, every level produces `Expr`, which is the+homogeneous choice; changing `term` to produce a `Term` and `expr` an `Expr`+makes a misplaced operator a type error, exactly as in the paper — at the cost+of an AST with one constructor per layer.++Note the FIRST set columns. They are not decoration: `'["atom", "term", "unary"]` says that entering `expr` can immediately enter any of those, and it+is what the acyclicity check consumes. Getting them wrong is a compile error,+so they double as a checked comment.++A FIRST set is written in **alphabetical order**, and with no repeats. The+order is not a matter of taste: it is what makes a set have exactly one+spelling, which in turn lets the compiler take the union of two FIRST sets in+one merge pass instead of re-scanning one of them for every element of the+other. Write `'["atom", "term", "unary"]`, not `'["term", "unary", "atom"]`;+the latter is a type error naming the first position that disagrees.++### 1.4 Precedence tables: absent, but not impossible++*(Willis & Wu, Pattern 1c: Precedence Tables.)*++Their `precedence` combinator folds a table of levels into the ladder:++```haskell+expr = precedence $+  sops InfixL [Add <$ char '+', Sub <$ char '-'] +<+  sops InfixL [Mul <$ char '*']                  +<+  sops Prefix [Neg <$ string "negate"]           +<+  Atom atom+```++**typed-peg does not provide this**, and adding it is more than a convenience+wrapper — but less than impossible, so it is worth being precise about what it+would take.++`Prec` in the paper is already a type-indexed structure: each `Op a b`+connects a layer producing `a` to one producing `b`, which is what makes+adding or removing a level a type error. A typed-peg version would have to+carry the `Ty` index as well, since every `PExp` is indexed by its nullability+and FIRST set:++```haskell+data Prec s env ty a where ...      -- sketch, not implemented+```++The good news is that the `Ty` arithmetic is tractable. Precedence layers are+anonymous `PExp` values rather than named non-terminals, and FIRST sets track+only non-terminal names — so every layer built from operators and a starred+tail has an empty FIRST set, and its nullability follows from `SeqTy`. What is+needed is a GADT whose indices compose the way `SeqTy` and `ChoiceTy` do, plus+`infixl1`/`infixr1`/`prefix`/`postfix` at the `PExp` level.++Until then, write the levels out as in §1.3. For four or five levels that is+barely longer than the table, and it keeps each level visible in the `Env`.++---++## 2. Lexing++### 2.1 Consume trailing whitespace only++*(Willis & Wu, Pattern 2a: Whitespace Combinators.)*++Their rule, which transfers unchanged: **every lexeme consumes the whitespace+*after* it, never before; one `fully` at the top consumes leading whitespace+and demands end of input.** Consuming leading whitespace inside a lexeme breaks+position reporting and makes it ambiguous who is responsible for a given space.++```haskell+ws :: PExp s env ('MkTy 'True '[]) s+ws = spanOf (fromRanges [(' ', ' '), ('\t', '\t'), ('\r', '\r'), ('\n', '\n')])++lexeme :: PExp s env ty a -> PExp s env (SeqTy ty ('MkTy 'True '[])) a+lexeme p = (\x _ -> x) <$>. p <*>. ws++eof :: PExp s env ('MkTy 'True '[]) ()+eof = Not AnyChar++fully :: PExp s env ty a+      -> PExp s env (SeqTy ('MkTy 'True '[])+                           (SeqTy ty ('MkTy 'True '[]))) a+fully p = (\_ x _ -> x) <$>. ws <*>. p <*>. eof+```++```+"12"     => OK "12"+"  12  " => OK "12"+"12 x"   => Fail+""       => Fail+```++Two typed-peg specifics. `ws` is a character class, so it compiles to a single+`Span` node and returns a chunk of the input — over `Text` that is a slice, and+when the item is unlabelled in a quasi-quoted rule the chunk is discarded+anyway. And `eof` is `Not AnyChar`, written `!.` in the quasi-quoter: a PEG+gets end-of-input from negative lookahead rather than from a primitive.++**`fully` matters more in a PEG than in `parsec`.** A PEG parser is happy to+succeed on a prefix:++```+"a:=1;" => OK [Asgn "a" (Num 1)] rest=";"+```++Nothing is wrong here — the grammar matched what it could. If you want the+whole input consumed you must say so, and `fully` is where you say it.++### 2.2 Tokens++*(Willis & Wu, Pattern 2bi: Tokenizing Combinators.)*++Same pattern: annotate terminals with `lexeme`, not the composite rules. Their+`token = lexeme . try` loses its `try` here, since ordered choice needs no+backtracking marker.++In a quasi-quoted grammar the usual spelling is a `ws` rule invoked after each+terminal, as `examples/Layout.hs` and the JSON benchmark do:++```+pair <- k:strlit ws ':' ws v:value  { (k, v) }+```++**Keep the `ws` calls at terminal boundaries and nowhere else.** A `ws` in the+middle of a composite rule is the same mistake as leading whitespace in a+lexeme: it makes two rules disagree about who owns the space between them.++### 2.3 Keywords are negative lookahead++*(Willis & Wu, Pattern 2bii: Keyword Combinators.)*++This is the pattern a PEG expresses best. The problem is that `string "negate"`+happily matches the prefix of `negatex`. Their answer is a `keyword` combinator+that checks no identifier character follows — which in `parsec` needs `try` to+undo the partial match.++In a PEG it is just `!`:++```haskell+keyword :: String -> PExp s env ('MkTy 'False '[]) ()+keyword k = (\_ _ -> ()) <$>. stringNE k <*>. Not (sat identCont)+```++or, in the quasi-quoter, `"negate" ![a-zA-Z0-9_]`.++```+"negate"   keyword => OK ()          | bare => OK ()+"negatex"  keyword => Fail           | bare => OK () rest="x"+"negate2"  keyword => Fail           | bare => OK () rest="2"+"negate x" keyword => OK () rest=" x"| bare => OK () rest=" x"+```++The `bare` column is the bug the pattern prevents: without the lookahead,+`negatex` parses as the keyword `negate` followed by the variable `x`.++**The same shape covers every longest-match ambiguity**, not only keywords:+`'<' !'='` is "less-than, but not the start of `<=`". See §5.1 for the+alternative spelling.++### 2.4 The quasi-quoter is the facade++*(Willis & Wu, Pattern 2c: Overloaded Strings.)*++Their goal is to write `"if" *> expr` and have the string literal quietly+become a tokenizing parser, via `IsString`. The quasi-quoter already provides+this, and more directly: inside `[pegRules| ... |]`, `"do"` *is* a string+literal in grammar syntax, `[a-z]` is a character class, and `/` is ordered+choice. There is no Haskell-level plumbing to hide.++The residue of the pattern still applies: **keep token definitions in one+place.** A rule named `ident` or `number` used everywhere beats the same+character class copy-pasted into five rules — not for concision, but because+the `Env` then names it, and a change happens once.++---++## 3. Building the AST++### 3.1 Lifted constructors++*(Willis & Wu, Pattern 3a: Lifted Constructors.)*++Their advice — put bookkeeping in a smart constructor so the parser reads like+the grammar — transfers unchanged, and typed-peg gives it an extra job. Because+a character class produces a chunk of the stream rather than a `String`, the+conversion belongs in the smart constructor rather than smeared through the+actions:++```haskell+mkNum :: Stream s => s -> Expr+mkNum = Num . read . chunkToString++mkVar :: Stream s => s -> Expr+mkVar = Var . chunkToString+```++```+atom <- '(' e:expr ')'+      / ds:[0-9]+                    { mkNum ds }+      / &[a-zA-Z_] cs:[a-zA-Z0-9_]+  { mkVar cs }+```++**Keep semantic actions one application wide.** An action is Haskell spliced+unhygienically into the generated code; a long one is hard to read in grammar+syntax and hard to debug when it fails to typecheck, because the error points+at the quasi-quote.++### 3.2 Deferred constructors, and the position gap++*(Willis & Wu, Pattern 3b: Deferred Constructors.)*++Their motivating example is source positions: a node needs the position from+*before* its first token, so the constructor is returned by a parser and+applied later.++**typed-peg cannot do this**, because no combinator exposes the current+position to a semantic action. `PState` tracks `stCol` and `stOff`, and+`PEG.Indent` uses columns for layout, but neither is reachable from `{ ... }`.+A grammar cannot annotate its AST with source locations.++What does transfer is the general form — returning a function to be applied+later, so that bookkeeping is decoupled from the parser. In typed-peg this is+just a rule whose result type is a function:++```haskell+type OpEnv = '[ '("op", 'EnvEntry ('MkTy 'False '[]) (Expr -> Expr -> Expr)) ]++addOp :: Stream s => Grammar s OpEnv _ (Expr -> Expr -> Expr)+addOp = Grammar [pegRules| op <- '+' { Add } / '-' { Sub } |] (nt @"op")+```++```+"+" => Add (Num 1) (Num 2)+"-" => Sub (Num 1) (Num 2)+"*" => Fail+```++The rule's result type is a function, and the chain rule applies it. This is the same defunctionalisation the paper describes, and it+removes the partial `error` case from §1.2's `chainl`.++See §6: exposing position is the single change that would unlock the most of+this paper.++---++## 4. Errors: the shape without the message++*(Willis & Wu, Patterns 4a Verified Errors and 4b Preventative Errors.)*++Their patterns are about *messages*: use `lookAhead` to check that an error is+warranted before raising it, and `notFollowedBy` to rule out input that would+otherwise produce a confusing failure further along.++**typed-peg has no error messages at all.** `Result` is++```haskell+data Result s a = OK a s s | Fail+```++There is no position, no expected set, no label. Both patterns are therefore+unavailable in their stated form.++The *rejection* half still works, and is worth using. Preventative errors+become preventative failures:++```+asgn <- &[a-zA-Z_] v:[a-zA-Z0-9_]+ ":=" e:expr !'<'  { mkAsgn v e }+```++— an assignment whose right-hand side is followed by `<` is rejected here+rather than half-consumed and rejected somewhere less obvious. You lose the+message; you keep the locality.++Positive lookahead `&e` is `Not (Not e)`, so the verification half of Pattern+4a is expressible as a guard even though nothing can be reported.++---++## 5. Patterns that are specific to PEGs++### 5.1 In an ordered choice, the longest alternative goes first++Nothing in the paper needs this, because `<|>` with `try` reconsiders. A PEG+commits to the first success:++```+op <- '<'  / '<='      -- WRONG: '<=' is never reached+op <- '<=' / '<'       -- right+```++The first line silently parses `a <= b` as `a < (= b)` and then fails+somewhere else entirely. There is no warning: both grammars typecheck, and both+have the same FIRST set.++**Pattern.** When two alternatives share a prefix, order them longest-first.+When that is awkward — because the alternatives are non-terminals whose lengths+are not obvious — use the §2.3 lookahead spelling instead, which states the+constraint locally rather than relying on the order of a list.++This is the one place where typed-peg's type-level machinery does *not* help,+and it is worth knowing that the acyclicity check is not a substitute for+thinking about the order.++### 5.2 Prefer a negated class to the `!c .` idiom++The traditional PEG spelling of "any character except a quote" is+`(!'"' .)*` — a negative lookahead followed by a wildcard, which inspects+every character twice. typed-peg's quasi-quoter accepts a negated class:++```+q <- '"' cs:[^"]* '"'        -- one bit test per character+q <- '"' cs:(!'"' c:.)* '"'  -- two passes per character, and a cons list+```++The two describe the same language. On the benchmark suite the class form+allocates **90 bytes per input byte against 209**, and runs about **1.5×+faster**. It also returns a chunk of the stream rather than a `[Char]`.++**Pattern.** Reach for `[^...]` whenever the lookahead is a single character.+Keep `!e` for the cases a class cannot express — a keyword boundary, a+multi-character sentinel, a non-terminal.++### 5.3 A starred character class returns a chunk++`[a-z]*` and `[a-z]+` compile to `Span`/`Span1` and produce a slice of the+input stream, not a `[Char]`. That has a consequence for a very common idiom:++```+ident <- c:[a-zA-Z_] cs:[a-zA-Z0-9_]*  { c : cs }+```++`c` is a `Char` and `cs` is a chunk. This still compiles if the grammar is+fixed to `String` — where a chunk *is* a `[Char]` — but a stream-polymorphic+grammar is rejected:++```+Couldn't match expected type 's' with actual type '[Char]'+  's' is a rigid type variable bound by the inferred type of+    identG :: Stream s => Grammar s (IdEnv s) (MkTy False '["ident"]) s+```++So the idiom quietly ties a grammar to one stream. The fix is also faster,+because it scans once instead of twice and copies nothing:++```+ident <- &[a-zA-Z_] cs:[a-zA-Z0-9_]+   { cs }+```++The positive lookahead pins the first character to the narrower class without+consuming it, then one span takes the whole identifier.++**Pattern.** When a token is "one character from class A, then characters from+class B" and A is a subset of B, write it as `&A B+`.++### 5.4 The `Env` is a specification, so write it first++The environment is not boilerplate to be derived from the rules — it is the+grammar's interface, and it is checked:++```haskell+type CalcEnv s =+  '[ '("expr" , 'EnvEntry ('MkTy 'False '["atom", "term", "unary"]) Expr)+   , ...+   ]+```++Each entry states three things: whether the rule can match the empty string,+which non-terminals it can enter first, and what it produces. All three are+verified against the rule bodies.++**Pattern.** Write the `Env` before the rules, as you would write a signature+before a function. When a rule's FIRST set surprises you, that is usually the+grammar telling you something — a rule that is unexpectedly nullable is often a+`*` that should have been a `+`.++Two practical notes. A rule whose result is a character-class repetition has+result type `s`, so its `Env` synonym takes the stream as a parameter+(`CalcEnv s`). And GHC's error when an entry is wrong points at the whole+quasi-quote, not at the offending rule — so add rules a few at a time.++### 5.5 Layout is a grammar concern, not a lexer concern++`PEG.Indent` gives rules a column relation, so indentation-sensitive syntax+stays in the grammar instead of being pushed into a layout-inserting lexer:++```+istmts <- ss:(ws st:|s:stmt|)+^>          -- each statement strictly indented+stmts  <- r:(ws '{' ... ws '}')^~         -- braces: any column+```++`^>`, `^~`, `^=` and `_~` attach a relation to a sub-expression. This has no+counterpart in the paper, whose language is layout-insensitive.++**Pattern.** Give the brace form and the layout form as two alternatives of one+rule, with the relation attached to each, rather than deciding between them+before parsing.++---++## 6. What typed-peg cannot do yet++Collected from above, in the order that would most help a user of this library:++1. **Source positions in semantic actions.** Blocks Pattern 3b's motivating+   use and any AST that records where its nodes came from. `PState` already+   carries `stOff` and `stCol`; what is missing is a `PExp` constructor that+   hands them to an action.+2. **Error messages.** `Result` is `OK` or `Fail`. Patterns 4a and 4b are+   about phrasing good errors, and neither can be expressed. This is the+   largest single gap between typed-peg and the paper.+3. **A `precedence` combinator.** Needs a `Ty`-indexed `Prec` GADT and+   `infixl1`/`infixr1`/`prefix`/`postfix` at the `PExp` level (§1.4). Real+   work, but the index arithmetic is tractable.+4. **Chain combinators.** `chainl1`/`chainr1` are a small, unblocked+   convenience: they would remove the hand-written `foldl` from every+   expression grammar.+5. **A token/lexeme vocabulary in the quasi-quoter.** `lexeme`, `keyword` and+   `fully` are ten lines each (§2.1, §2.3) but every user writes them again.++None of 1, 2, 4 or 5 is blocked by the type-level design; they are absent+rather than impossible.++---++## Reading the examples++```bash+cabal test typed-peg-examples+```++runs `examples/Patterns.hs` along with the rest, printing the output quoted+throughout this document. The grammars are in:++- [`examples/Patterns.hs`](examples/Patterns.hs) — every fragment above+- [`examples/Arith.hs`](examples/Arith.hs) — the minimal precedence ladder+- [`examples/Layout.hs`](examples/Layout.hs) — indentation-sensitive `do`+- [`bench/Bench/Peg.hs`](bench/Bench/Peg.hs) — JSON, CSV, and the two+  quoted-string spellings of §5.2
src/PEG.hs view
@@ -19,7 +19,9 @@ -- -- See the @examples/@ directory for complete working grammars. module PEG-  ( module PEG.Type+  ( module PEG.CharSet+  , module PEG.Stream+  , module PEG.Type   , module PEG.TyLevel   , module PEG.Member   , module PEG.Indent@@ -28,7 +30,9 @@   , module PEG.Parse   ) where +import PEG.CharSet import PEG.Grammar+import PEG.Stream import PEG.Indent import PEG.Member import PEG.Parse
+ src/PEG/CharSet.hs view
@@ -0,0 +1,105 @@+{-# LANGUAGE BangPatterns #-}++-- | Compact character sets used by the 'PEG.Syntax.Sat' combinator.+--+-- A character class such as @[a-zA-Z0-9_]@ used to be compiled into a chain of+-- 63 ordered choices, so matching a single character could cost 63 parser+-- steps.  A 'CharSet' answers the same question with one bit test.+--+-- The Latin-1 range (@\\0@ .. @\\255@), which covers essentially every class+-- that appears in a practical grammar, is stored as a 256-bit bitmap held in+-- four 'Word64's.  Characters above that range fall back to a list of ranges.+module PEG.CharSet+  ( CharSet (..)+  , memberCS+  , fromRanges+  , notInRanges+  , fromList+  , singletonCS+  , complementCS+  , nullCS+  , anyCS+  ) where++import Data.Bits (setBit, testBit)+import Data.Char (chr, ord)+import Data.Word (Word64)++-- | A set of characters.  The four 'Word64' fields form a bitmap of the+-- Latin-1 range; 'csWide' holds any ranges that reach beyond it.+--+-- Negation is a flag rather than an actual complement, so a negated class is+-- exactly as cheap to test as a positive one and stays exact for the whole of+-- 'Char' (complementing the ranges above Latin-1 explicitly would not).+data CharSet = CharSet+  { csNeg  :: !Bool+  , csB0   :: !Word64+  , csB1   :: !Word64+  , csB2   :: !Word64+  , csB3   :: !Word64+  , csWide :: ![(Char, Char)]+  }+  deriving (Eq, Show)++-- | Is the character a member of the set?  @O(1)@ for Latin-1 characters.+memberCS :: Char -> CharSet -> Bool+memberCS c cs = csNeg cs /= rawMember c cs+{-# INLINE memberCS #-}++-- | Membership ignoring the negation flag.+rawMember :: Char -> CharSet -> Bool+rawMember c (CharSet _ b0 b1 b2 b3 wide)+  | n < 64    = testBit b0 n+  | n < 128   = testBit b1 (n - 64)+  | n < 192   = testBit b2 (n - 128)+  | n < 256   = testBit b3 (n - 192)+  | otherwise = inWide wide+  where+    !n = ord c+    inWide []              = False+    inWide ((lo, hi) : rs) = (n >= ord lo && n <= ord hi) || inWide rs+{-# INLINE rawMember #-}++-- | Build a set from a list of inclusive character ranges.+fromRanges :: [(Char, Char)] -> CharSet+fromRanges = mkRanges False++-- | The complement of 'fromRanges': every character /outside/ the given+-- ranges.  This is what the quasi-quoter emits for @[^\"]@.+notInRanges :: [(Char, Char)] -> CharSet+notInRanges = mkRanges True++-- | Flip a set\'s polarity.+complementCS :: CharSet -> CharSet+complementCS cs = cs { csNeg = not (csNeg cs) }++mkRanges :: Bool -> [(Char, Char)] -> CharSet+mkRanges neg rs = CharSet neg (word 0) (word 64) (word 128) (word 192) wide+  where+    lows = [ n | (lo, hi) <- rs, n <- [ord lo .. min 255 (ord hi)] ]++    word base = go 0 lows+      where+        go !w []       = w+        go !w (n : ns)+          | n >= base && n < base + 64 = go (setBit w (n - base)) ns+          | otherwise                  = go w ns++    wide = [ (max lo (chr 256), hi) | (lo, hi) <- rs, ord hi > 255 ]++-- | Build a set from an explicit list of characters.+fromList :: [Char] -> CharSet+fromList cs = fromRanges [ (c, c) | c <- cs ]++-- | The set containing exactly one character.+singletonCS :: Char -> CharSet+singletonCS c = fromRanges [(c, c)]++-- | The set of every character.  This is what @.*@ compiles to.+anyCS :: CharSet+anyCS = notInRanges []++-- | Is the set empty?+nullCS :: CharSet -> Bool+nullCS (CharSet False 0 0 0 0 []) = True+nullCS _                          = False
src/PEG/Grammar.hs view
@@ -29,35 +29,44 @@  -- | A typed, heterogeneous list of named grammar rules. ----- @'Rules' env defs@ is a list of rules whose bodies reference non-terminals--- in @env@ and whose definitions together form @defs@.-data Rules (env :: Env) (defs :: Env) where-  RNil  :: Rules env '[]-  RCons :: Name s-        -> PExp env ty a-        -> Rules env rest-        -> Rules env ('(s, 'EnvEntry ty a) ': rest)+-- @'Rules' s env defs@ is a list of rules over the stream @s@ whose bodies+-- reference non-terminals in @env@ and whose definitions together form+-- @defs@.+data Rules (s :: Type) (env :: Env) (defs :: Env) where+  RNil  :: Rules s env '[]+  RCons :: Name n+        -> PExp s env ty a+        -> Rules s env rest+        -> Rules s env ('(n, 'EnvEntry ty a) ': rest)  type family Acyclic (env :: Env) :: Constraint where-  Acyclic '[]                             = ()-  Acyclic ('(s, 'EnvEntry ty _) ': rest) =-    (NotLeftRec s (Elem s (First ty)) ty, Acyclic rest)+  Acyclic '[]                                      = ()+  Acyclic ('(s, 'EnvEntry ('MkTy _ f) _) ': rest) =+    (NotLeftRec s (Elem s f) f, Acyclic rest) -type family NotLeftRec (s :: Symbol) (b :: Bool) (ty :: Ty) :: Constraint where-  NotLeftRec _ 'False _  = ()-  NotLeftRec s 'True  ty =+type family NotLeftRec (s :: Symbol) (b :: Bool)+                       (f :: [Symbol]) :: Constraint where+  NotLeftRec _ 'False _ = ()+  NotLeftRec s 'True  f =     TypeError ('Text "Left-recursive non-terminal: " ':<>: 'ShowType s          ':$$: 'Text "Its head set already contains itself: "-               ':<>: 'ShowType (First ty)+               ':<>: 'ShowType f          ':$$: 'Text "Violates the acyclicity condition i `notElem` Gamma(i).F.") --- | A complete PEG grammar: a set of mutually recursive rules and a start--- expression.+-- | A complete PEG grammar over the stream @s@: a set of mutually recursive+-- rules and a start expression. --+-- A 'Grammar' is monomorphic in its stream.  To reuse one grammar across+-- several stream types, give it a signature of the form+-- @forall s. 'PEG.Stream.Stream' s => Grammar s Env ty a@ — but note that+-- doing so turns the value into a function of a dictionary, so the compiled+-- parser is no longer shared between calls.  Prefer a monomorphic top-level+-- signature.+-- -- Constructing a 'Grammar' value discharges the 'Acyclic' constraint, so -- any left-recursion in @env@ becomes a compile-time type error.-data Grammar (env :: Env) (startTy :: Ty) (startA :: Type) where+data Grammar (s :: Type) (env :: Env) (startTy :: Ty) (startA :: Type) where   Grammar :: Acyclic env-          => Rules env env-          -> PExp env startTy startA-          -> Grammar env startTy startA+          => Rules s env env+          -> PExp s env startTy startA+          -> Grammar s env startTy startA
src/PEG/Indent.hs view
@@ -76,6 +76,13 @@  data RelD = RelD   { rdName      :: String+  , rdTotal     :: !Bool+    -- ^ 'True' when the relation places no constraint at all on columns, i.e.+    -- when @'preimage' rd i == 'fullI'@ and @'image' rd j@ leaves the+    -- candidate interval untouched, for every non-empty @i@ and every @j@.+    -- Only 'anyR' satisfies this.  The parser uses the flag to skip all+    -- interval arithmetic on grammars that do not use layout, which is the+    -- overwhelmingly common case.   , rdDom       :: Interval   , rdLo        :: Int -> Int   , rdHi        :: Int -> Bound@@ -126,6 +133,7 @@ eqR :: Rel "=" eqR = Rel RelD   { rdName      = "="+  , rdTotal     = False   , rdDom       = fullI   , rdLo        = id   , rdHi        = Fin@@ -143,6 +151,7 @@ gapD :: String -> Int -> RelD gapD name k = RelD   { rdName      = name+  , rdTotal     = False   , rdDom       = Interval k Inf   , rdLo        = const 0   , rdHi        = \i -> Fin (i - k)@@ -163,6 +172,7 @@ anyR :: Rel "~" anyR = Rel RelD   { rdName      = "~"+  , rdTotal     = True   , rdDom       = fullI   , rdLo        = const 0   , rdHi        = const Inf@@ -177,6 +187,7 @@ constR :: Int -> Rel "const" constR c = Rel RelD   { rdName      = "const " ++ show c+  , rdTotal     = False   , rdDom       = singletonI c   , rdLo        = const 0   , rdHi        = const Inf@@ -191,6 +202,7 @@ offsetR :: Int -> Rel "offset" offsetR k = Rel RelD   { rdName      = "+" ++ show k+  , rdTotal     = False   , rdDom       = Interval k Inf   , rdLo        = \i -> i - k   , rdHi        = \i -> Fin (i - k)
src/PEG/Member.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE AllowAmbiguousTypes   #-} {-# LANGUAGE DataKinds             #-} {-# LANGUAGE FlexibleContexts      #-} {-# LANGUAGE FlexibleInstances     #-}@@ -5,16 +6,27 @@ {-# LANGUAGE KindSignatures        #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE ScopedTypeVariables   #-}+{-# LANGUAGE TypeApplications      #-} {-# LANGUAGE TypeFamilies          #-} {-# LANGUAGE TypeOperators         #-} {-# LANGUAGE UndecidableInstances  #-}  -- | Membership witnesses for heterogeneous type-level environments. ----- 'Member' is a proof that a name @s@ with type @ty@ and result @a@ is--- present in the environment @env@.  'KnownMember' is the class that allows--- the proof to be materialised from type information at runtime, enabling--- non-terminal lookup during parsing.+-- 'Member' is a proof that a name @s@ with result type @a@ is present in the+-- environment @env@.  'KnownMember' is the class that allows the proof to be+-- materialised from type information at runtime, enabling non-terminal lookup+-- during parsing.+--+-- == Why the 'PEG.Type.Ty' is not an index+--+-- The witness deliberately does /not/ record the non-terminal's+-- 'PEG.Type.Ty'.  Resolving @KnownMember s env a@ walks @env@ one instance at+-- a time, and every index of the class is carried along — and re-normalised —+-- at each of those steps.  A 'PEG.Type.Ty' carries a FIRST set, so an index+-- for it makes each step cost @O(|env|)@ instead of @O(1)@.  Nothing needs+-- it: 'Here' binds the entry's @ty@ existentially, which is enough to pull+-- the matching rule out of a rule table. module PEG.Member   ( Member (..)   , KnownMember (..)@@ -22,35 +34,41 @@  import Data.Kind    (Type) import Data.Proxy   (Proxy (..))-import GHC.TypeLits (ErrorMessage (..), Symbol, TypeError)+import GHC.TypeLits (CmpSymbol, ErrorMessage (..), Symbol, TypeError)  import PEG.Type-import PEG.TyLevel (SymEq) -data Member (s :: Symbol) (env :: Env) (ty :: Ty) (a :: Type) where-  Here  :: Member s ('(s, 'EnvEntry ty a) ': rest) ty a-  There :: Member s rest ty a -> Member s (e ': rest) ty a+-- | @'Member' s env a@ witnesses that @env@ binds the name @s@ to a rule+-- returning @a@, and records /where/ in @env@ that binding is.+data Member (s :: Symbol) (env :: Env) (a :: Type) where+  Here  :: Member s ('(s, 'EnvEntry ty a) ': rest) a+  There :: Member s rest a -> Member s (e ': rest) a -class KnownMember (s :: Symbol) (env :: Env) (ty :: Ty) (a :: Type) where-  member :: Member s env ty a+class KnownMember (s :: Symbol) (env :: Env) (a :: Type) where+  member :: Member s env a  instance TypeError ('Text "Undefined non-terminal: " ':<>: 'ShowType s                ':$$: 'Text "The grammar has no rule for this name.")-      => KnownMember s '[] ty a where+      => KnownMember s '[] a where   member = error "PEG.Member: unreachable" -instance KnownMember' (SymEq s t) s ('(t, e) ': rest) ty a-      => KnownMember s ('(t, e) ': rest) ty a where-  member = member' (Proxy :: Proxy (SymEq s t))+-- Dispatch on 'CmpSymbol' directly rather than through a @SymEq@ wrapper:+-- that is one fewer type-family application to reduce per entry scanned, and+-- an environment is scanned once per occurrence of every non-terminal.+instance KnownMemberStep (CmpSymbol s t) s ('(t, e) ': rest) a+      => KnownMember s ('(t, e) ': rest) a where+  member = memberStep (Proxy :: Proxy (CmpSymbol s t)) -class KnownMember' (b :: Bool) (s :: Symbol) (env :: Env)-                   (ty :: Ty) (a :: Type) where-  member' :: Proxy b -> Member s env ty a+class KnownMemberStep (o :: Ordering) (s :: Symbol) (env :: Env) (a :: Type) where+  memberStep :: Proxy o -> Member s env a -instance (s ~ t, e ~ 'EnvEntry ty a)-      => KnownMember' 'True s ('(t, e) ': rest) ty a where-  member' _ = Here+-- The entry is taken apart in the instance head, so @ty@ is bound by+-- matching and never has to be threaded through the class.+instance (s ~ t) => KnownMemberStep 'EQ s ('(t, 'EnvEntry ty a) ': rest) a where+  memberStep _ = Here -instance KnownMember s rest ty a-      => KnownMember' 'False s ('(t, e) ': rest) ty a where-  member' _ = There member+instance KnownMember s rest a => KnownMemberStep 'LT s ('(t, e) ': rest) a where+  memberStep _ = There member++instance KnownMember s rest a => KnownMemberStep 'GT s ('(t, e) ': rest) a where+  memberStep _ = There member
src/PEG/Parse.hs view
@@ -1,70 +1,121 @@+{-# LANGUAGE BangPatterns        #-} {-# LANGUAGE DataKinds           #-} {-# LANGUAGE GADTs               #-}+{-# LANGUAGE KindSignatures      #-}+{-# LANGUAGE MagicHash           #-} {-# LANGUAGE RankNTypes          #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeAbstractions    #-} {-# LANGUAGE TypeApplications    #-} {-# LANGUAGE TypeFamilies        #-} {-# LANGUAGE TypeOperators       #-}+{-# LANGUAGE UnboxedSums         #-}+{-# LANGUAGE UnboxedTuples       #-} --- | Running a 'Grammar' against a 'String'.+-- | Running a 'Grammar' against an input stream. -- -- The top-level entry points are 'parse' (uses 'defaultOpts') and 'parseWith' -- (accepts custom 'Opts' for indentation-sensitive parsing).  Both return a -- 'Result' that records the matched value, the consumed prefix, and the -- remaining suffix.+--+-- The input can be any "PEG.Stream" instance: 'String', strict or lazy+-- 'Data.Text.Text', strict or lazy 'Data.ByteString.ByteString'.+--+-- == Compiling once, parsing many times+--+-- 'parseWith' is written so that @'parseWith' opts g@ is a /closure/ that has+-- already traversed the grammar: every non-terminal reference has been+-- resolved to a function, and no 'PExp' constructor is examined again while+-- input is being consumed.  Bind it once and reuse it:+--+-- @+-- myParser :: String -> Result String Exp+-- myParser = parse myGrammar     -- compiled once, at first use+-- @+--+-- Writing @'parse' myGrammar input@ inline inside a loop instead re-does the+-- traversal on every call.  Give the binding a /monomorphic/ signature: a+-- grammar left polymorphic in its stream is a function of a 'Stream'+-- dictionary rather than a constant, so nothing is shared between calls.+--+-- == Why the result of a step is an unboxed sum+--+-- A compiled step returns @(# (# #) | (# a, 'PState' s #) #)@ rather than+-- @'Maybe' (a, 'PState' s)@.  The two are isomorphic, but the unboxed sum+-- lives in registers: a step that succeeds no longer allocates a @Just@ /and/+-- a pair on top of the new state, and a step that fails allocates nothing at+-- all.  Because the intermediate results of 'Seq' and 'Map' never escape,+-- this makes those two constructors — the ones the quasi-quoter emits for+-- every single grammar item — allocation-free. module PEG.Parse   ( Result (..)   , parse   , parseWith-  , eval+  , compileGrammar+  , Step+  , Res   , Opts (..)   , defaultOpts-  , Input   , PState (..)-  , columns+  , nextCol   ) where +import Data.Kind (Type)+import qualified Data.ByteString as B+import qualified Data.Text       as T++import PEG.CharSet (CharSet, memberCS) import PEG.Grammar import PEG.Indent import PEG.Member+import PEG.Stream import PEG.Syntax import PEG.Type-import PEG.TyLevel (Lookup)  -- | The result of running a grammar. -- -- @'OK' a consumed rest@ means the grammar matched, producing value @a@. -- @consumed@ is the prefix of the input that was consumed; @rest@ is the -- remaining input.-data Result a-  = OK a String String+data Result s a+  = OK a s s   | Fail   deriving (Show, Eq) --- | A string annotated with column positions, as produced by 'columns'.-type Input = [(Char, Int)]- -- | Internal parser state.-data PState = PState-  { stInput :: Input     -- ^ Remaining input with column positions.+--+-- The column of the character at the head of 'stInput' is carried alongside+-- the input rather than being precomputed for the whole stream, so nothing+-- proportional to the input is ever allocated up front.+data PState s = PState+  { stInput :: !s        -- ^ Remaining input.+  , stCol   :: !Int      -- ^ Column of the head of 'stInput'.+  , stOff   :: !Int      -- ^ Characters consumed so far.   , stCands :: !Interval -- ^ Current candidate column interval.   , stAlign :: !Bool     -- ^ Whether the next token must be aligned.   } --- | Annotate every character in a string with its column position.--- Tab stops are expanded according to @tabWidth@.-columns :: Int -> String -> Input-columns tabWidth = go 0-  where-    go _ []     = []-    go c (x:xs) = (x, c) : go (next c x) xs+-- | What a compiled step returns: either failure (the left injection, which+-- carries nothing) or a value together with the state after it.+--+-- This is @'Maybe' (a, 'PState' s)@ with the two boxes removed.+type Res s a = (# (# #) | (# a, PState s #) #) -    next _ '\n' = 0-    next c '\t'-      | tabWidth > 1 = ((c `div` tabWidth) + 1) * tabWidth-      | otherwise    = c + 1-    next c _    = c + 1+-- | A compiled parser: it still takes the ambient column relation, because a+-- rule body inherits the relation in force at its call site.+type Step s a = RelD -> PState s -> Res s a +-- | Column of the character following @c@, given a column of @c@ and a tab+-- width.+nextCol :: Int -> Int -> Char -> Int+nextCol _  _ '\n' = 0+nextCol tw c '\t'+  | tw > 1        = ((c `div` tw) + 1) * tw+  | otherwise     = c + 1+nextCol _  c _    = c + 1+{-# INLINE nextCol #-}+ -- | Parser configuration. data Opts = Opts   { optTokenMode :: RelD     -- ^ Default column relation between tokens.@@ -81,102 +132,348 @@   }  -- | Run a grammar with 'defaultOpts'.-parse :: Grammar env ty a -> String -> Result a+parse :: Stream s => Grammar s env ty a -> s -> Result s a parse = parseWith defaultOpts+{-# INLINABLE parse #-}+{-# SPECIALIZE parse :: Grammar String env ty a -> String -> Result String a #-}+{-# SPECIALIZE parse :: Grammar T.Text env ty a -> T.Text -> Result T.Text a #-}+{-# SPECIALIZE parse+      :: Grammar B.ByteString env ty a -> B.ByteString -> Result B.ByteString a #-}  -- | Run a grammar with custom 'Opts'.-parseWith :: Opts -> Grammar env ty a -> String -> Result a-parseWith opts (Grammar rules start) input =-  case eval rules start (optTokenMode opts) st0 of-    Nothing      -> Fail-    Just (a, st) ->-      let n = length input - length (stInput st)-      in OK a (take n input) (drop n input)+--+-- Partially applying this to the options and the grammar yields a compiled+-- parser; see the note at the top of this module.+parseWith :: forall s env ty a.+             Stream s => Opts -> Grammar s env ty a -> s -> Result s a+parseWith opts g = run   where-    st0 = PState-      { stInput = columns (optTabWidth opts) input-      , stCands = optCands opts-      , stAlign = False-      }+    step = compileGrammar (optTabWidth opts) g+    tau0 = optTokenMode opts --- | Low-level evaluator: run a 'PExp' against a 'PState' under a given column--- relation.  Exposed for advanced use; most callers should use 'parse' or--- 'parseWith'.-eval :: forall env ty a-      . Rules env env-     -> PExp env ty a-     -> RelD-     -> PState-     -> Maybe (a, PState)-eval rules = go+    run input = case step tau0 (PState input 0 0 (optCands opts) False) of+      (# (# #) | #)       -> Fail+      (# | (# a, st #) #) -> OK a (takeS (stOff st) input) (stInput st)+{-# INLINABLE parseWith #-}+{-# SPECIALIZE parseWith+      :: Opts -> Grammar String env ty a -> String -> Result String a #-}+{-# SPECIALIZE parseWith+      :: Opts -> Grammar T.Text env ty a -> T.Text -> Result T.Text a #-}+{-# SPECIALIZE parseWith+      :: Opts -> Grammar B.ByteString env ty a+      -> B.ByteString -> Result B.ByteString a #-}++--------------------------------------------------------------------------------+-- Compilation+--------------------------------------------------------------------------------++-- | A rule table in which every body has already been compiled to a 'Step'.+-- Built with a knot so that mutually recursive rules resolve to each other's+-- closures.+data CRules (s :: Type) (env :: Env) (defs :: Env) where+  CNil  :: CRules s env '[]+  CCons :: Step s a+        -> CRules s env rest+        -> CRules s env ('(n, 'EnvEntry ty a) ': rest)++clookup :: Member n defs a -> CRules s env defs -> Step s a+clookup Here      (CCons f _)    = f+clookup (There m) (CCons _ rest) = clookup m rest++-- | Traverse the grammar once and return a closure that consumes input.+--+-- The traversal resolves every non-terminal reference to the corresponding+-- compiled rule, so at parse time a non-terminal costs one indirect call+-- instead of a walk down the rule list.+compileGrammar :: forall s env ty a.+                  Stream s => Int -> Grammar s env ty a -> Step s a+compileGrammar tw (Grammar rules start) = compileE tw table start   where-    go :: forall t b. PExp env t b -> RelD -> PState -> Maybe (b, PState)-    go (Pure x) _ st = Just (x, st)+    table :: CRules s env env+    table = build rules -    go (Term c) tau st = do-      (x, st') <- terminal tau st-      if x == c then Just (c, st') else Nothing+    build :: forall defs. Rules s env defs -> CRules s env defs+    build RNil                = CNil+    build (RCons _ body rest) = CCons (compileE tw table body) (build rest)+{-# INLINABLE compileGrammar #-}+{-# INLINABLE compileE #-}+-- Without these the whole parse runs through a 'Stream' dictionary, and the+-- per-character path stops being allocation-free.  Callers using another+-- stream should mark their own monomorphic parser bindings INLINABLE.+{-# SPECIALIZE compileGrammar+      :: Int -> Grammar String env ty a -> Step String a #-}+{-# SPECIALIZE compileGrammar+      :: Int -> Grammar T.Text env ty a -> Step T.Text a #-}+{-# SPECIALIZE compileGrammar+      :: Int -> Grammar B.ByteString env ty a -> Step B.ByteString a #-} -    go AnyChar tau st = terminal tau st+-- | Does this class avoid the two characters whose column advance is not+-- simply @+1@?  When it does, the column after a matched run is the column+-- before it plus the run's length, and no fold is needed.+simpleCS :: CharSet -> Bool+simpleCS cs = not (memberCS '\n' cs) && not (memberCS '\t' cs) -    go (NT (_ :: Name s)) tau st =-      go (ruleFor (member :: Member s env (TyOf (Lookup s env))-                                          (ResOf (Lookup s env)))-                  rules)-         tau st+compileE :: forall s env ty a.+            Stream s => Int -> CRules s env env -> PExp s env ty a -> Step s a+compileE tw table = comp+  where+    -- Select the stream operations once per compiled grammar.  Leaving them+    -- as class-method applications would repeat the dictionary lookup on+    -- every character.+    !uncons  = unconsS       :: s -> (# (# #) | (# Char, s #) #)+    !spanS'  = spanS         :: (Char -> Bool) -> s -> (s, s)+    !lenS'   = lengthS       :: s -> Int+    !foldS'  = foldlS'       :: (Int -> Char -> Int) -> Int -> s -> Int+    !toStr   = chunkToString :: s -> String+    !packS   = packString    :: String -> s+    !emptyS  = packS [] -    go (Seq ef ex) tau st = do-      (f, st')  <- go ef tau st-      (x, st'') <- go ex tau st'-      pure (f x, st'')+    comp :: forall t b. PExp s env t b -> Step s b -    go (Choice e1 e2) tau st = case go e1 tau st of-      Just r  -> Just r-      Nothing -> go e2 tau st+    comp (Pure x) = \_ st -> (# | (# x, st #) #) -    go (Star e) tau st = Just (starLoop (go e tau) st)+    comp (Term c) = satStep (c ==) -    go (Not e) tau st = case go e tau st of-      Just _  -> Nothing-      Nothing -> Just ((), st)+    comp (Sat cs) = satStep (\c -> memberCS c cs) -    go (Map f e) tau st = do-      (x, st') <- go e tau st-      pure (f x, st')+    comp AnyChar  = satStep (const True) -    go (Indent rho e) tau st = do-      (x, st') <- go e tau st { stCands = preimage rd (stCands st) }-      pure ( x-           , st' { stCands = interI (stCands st) (image rd (stCands st')) } )+    comp (Str lit) = litStep lit++    -- A run of a character class, returned as a chunk of the stream.  On+    -- 'Data.Text.Text' this is a slice: no copy, no cons cells.+    comp (Span  cs) = spanChunk (\c -> memberCS c cs) (simpleCS cs) False+    comp (Span1 cs) = spanChunk (\c -> memberCS c cs) (simpleCS cs) True++    -- 'ty' and 'a' come from the constructor's own equality+    -- @Lookup n env ~ 'EnvEntry ty a@, so no type family has to be reduced+    -- here at all.+    comp (NT @n _) = clookup (member @n @env) table++    -- Neither this nor 'Map' below allocates: the intermediate results travel+    -- in registers, so a quasi-quoted rule of @n@ items costs @n@ calls and+    -- nothing else.+    comp (Seq ef ex) =+      let pf = comp ef+          px = comp ex+      in \tau st -> case pf tau st of+           (# (# #) | #)       -> (# (# #) | #)+           (# | (# f, s1 #) #) -> case px tau s1 of+             (# (# #) | #)       -> (# (# #) | #)+             (# | (# x, s2 #) #) -> (# | (# f x, s2 #) #)++    comp (Choice e1 e2) =+      let p = comp e1+          q = comp e2+      in \tau st -> case p tau st of+           (# (# #) | #) -> q tau st+           r             -> r++    -- A hand-written @'Star' ('Sat' cs)@ still produces a @['Char']@ rather+    -- than a chunk, so it needs its own scanner.  The quasi-quoter emits+    -- 'Span' instead, but 'PExp' values built by hand can be either.+    comp (Star (Sat cs)) = spanList (\c -> memberCS c cs) (simpleCS cs)+    comp (Star (Term c)) = spanList (c ==) (c /= '\n' && c /= '\t')+    comp (Star AnyChar)  = spanList (const True) False++    comp (Star e) =+      let p = comp e+          go acc tau st = case p tau st of+            (# (# #) | #)        -> (# | (# reverse acc, st #) #)+            (# | (# x, st' #) #) -> go (x : acc) tau st'+      in go []++    -- A negative lookahead at a single character only needs to peek.+    comp (Not (Sat cs))   = notCharStep (\c -> memberCS c cs)+    comp (Not (Term c))   = notCharStep (c ==)+    comp (Not AnyChar)    = notCharStep (const True)+    -- @!e+@ succeeds exactly when the next character is not in the class, so+    -- it is the same peek.  Without this case the generic 'Not' below would+    -- run the whole scan to answer a one-character question.+    comp (Not (Span1 cs)) = notCharStep (\c -> memberCS c cs)+    -- @!e*@ can never succeed: the star always matches, if only the empty+    -- run.  Say so directly rather than scanning the input to find out.+    comp (Not (Span _))   = \_ _ -> (# (# #) | #)++    comp (Not e) =+      let p = comp e+      in \tau st -> case p tau st of+           (# (# #) | #) -> (# | (# (), st #) #)+           _             -> (# (# #) | #)++    comp (Map f e) =+      let p = comp e+      in \tau st -> case p tau st of+           (# (# #) | #)       -> (# (# #) | #)+           (# | (# x, s1 #) #) -> (# | (# f x, s1 #) #)++    comp (Indent rho e) =+      let p  = comp e+          !rd = relD rho+      in \tau st ->+           case p tau st { stCands = preimage rd (stCands st) } of+             (# (# #) | #)       -> (# (# #) | #)+             (# | (# x, s1 #) #) ->+               (# | (# x+                     , s1 { stCands = interI (stCands st)+                                             (image rd (stCands s1)) } #) #)++    comp (Position sigma e) =+      let p  = comp e+          !rd = relD sigma+      in \_ st -> p rd st++    comp (Align e) =+      let p = comp e+      in \tau st -> case p tau st { stAlign = True } of+           (# (# #) | #)       -> (# (# #) | #)+           (# | (# x, s1 #) #) ->+             (# | (# x, s1 { stAlign = stAlign st && stAlign s1 } #) #)++    ------------------------------------------------------------------------+    -- Terminals.  These live here rather than at the top level so that they+    -- close over the hoisted stream operations above.+    ------------------------------------------------------------------------++    -- | Match one character satisfying a predicate.  The predicate is tested+    -- /before/ any column bookkeeping, so a failing alternative costs one+    -- comparison and nothing else.+    satStep :: (Char -> Bool) -> Step s Char+    satStep p = \tau st -> case uncons (stInput st) of+      (# | (# x, xs #) #) | p x -> advance tw tau st x xs+      _                         -> (# (# #) | #)++    -- | Match a literal string.  On the fast path the whole literal is+    -- matched with a single loop and a single new 'PState'; otherwise it goes+    -- character by character so that column bookkeeping stays exactly as it+    -- would be for the equivalent chain of 'Term's.+    --+    -- The result is the literal itself, so no chunk is built.+    litStep :: String -> Step s String+    litStep lit = \tau st ->+      if plainly tau st+        then fast (stInput st) lit (stCol st) (stOff st) (stCands st)+        else slow lit tau st       where-        rd = relD rho+        fast rest [] !col !off cands =+          (# | (# lit, PState rest col off cands False #) #)+        fast rest (c : cs) !col !off cands = case uncons rest of+          (# | (# x, xs #) #)+            | x == c -> fast xs cs (nextCol tw col x) (off + 1) cands+          _          -> (# (# #) | #) -    go (Position sigma e) _ st = go e (relD sigma) st+        slow []       _   st = (# | (# lit, st #) #)+        slow (c : cs) tau st = case uncons (stInput st) of+          (# | (# x, xs #) #)+            | x == c -> case advance tw tau st x xs of+                          (# (# #) | #)        -> (# (# #) | #)+                          (# | (# _, st' #) #) -> slow cs tau st'+          _          -> (# (# #) | #) -    go (Align e) tau st = do-      (x, st') <- go e tau st { stAlign = True }-      pure (x, st' { stAlign = stAlign st && stAlign st' })+    -- | A run of a character class, returned as a chunk.+    --+    -- On the fast path this is one native @span@ — a slice for 'Text' and+    -- 'ByteString' — plus, when the class can contain a newline or a tab, one+    -- fold to find the resulting column.+    spanChunk :: (Char -> Bool) -> Bool -> Bool -> Step s s+    spanChunk p simple atLeastOne = go+      where+        go tau st+          | plainly tau st = case uncons (stInput st) of+              -- Peek before spanning.  A class that cannot match the very+              -- next character is the common case in an ordered choice, and+              -- calling 'spanS' just to be handed an empty prefix would+              -- allocate a pair on every failed alternative.+              (# | (# c, _ #) #) | p c -> chunk tau st+              _ | atLeastOne -> (# (# #) | #)+                | otherwise  -> (# | (# emptyS, st #) #)+          | otherwise = loop [] tau st -terminal :: RelD -> PState -> Maybe (Char, PState)-terminal tau (PState input cands aligned) = case input of-  []            -> Nothing-  ((x, i) : xs)-    | aligned   ->-        if memberI i cands-          then Just (x, PState xs (singletonI i) False)-          else Nothing-    | otherwise ->-        if memberI i (preimage tau cands)-          then Just (x, PState xs (interI cands (image tau (singletonI i))) False)-          else Nothing+        chunk _ st = case spanS' p (stInput st) of+          (pre, rest) ->+            let !n    = lenS' pre+                !col' = if simple then stCol st + n+                                  else foldS' (nextCol tw) (stCol st) pre+            in (# | (# pre+                     , PState rest col' (stOff st + n)+                              (stCands st) False #) #) -starLoop :: (PState -> Maybe (a, PState)) -> PState -> ([a], PState)-starLoop step = loop+        -- The layout-sensitive path: every character has to go through the+        -- interval arithmetic, so the chunk is rebuilt from the characters.+        loop acc tau st = case satStep p tau st of+          (# | (# x, st' #) #) -> loop (x : acc) tau st'+          (# (# #) | #)+            | atLeastOne && null acc -> (# (# #) | #)+            | otherwise -> (# | (# packS (reverse acc), st #) #)++    -- | A run of a character class, returned as a @['Char']@.  Only reachable+    -- from a hand-written @'Star' ('Sat' _)@; the quasi-quoter emits 'Span'.+    spanList :: (Char -> Bool) -> Bool -> Step s String+    spanList p simple = go+      where+        go tau st+          | plainly tau st = case uncons (stInput st) of+              (# | (# c, _ #) #) | p c -> case spanS' p (stInput st) of+                (pre, rest) ->+                  let !n    = lenS' pre+                      !col' = if simple then stCol st + n+                                        else foldS' (nextCol tw) (stCol st) pre+                  in (# | (# toStr pre+                           , PState rest col' (stOff st + n)+                                    (stCands st) False #) #)+              _ -> (# | (# [], st #) #)+          | otherwise = loop [] tau st++        loop acc tau st = case satStep p tau st of+          (# | (# x, st' #) #) -> loop (x : acc) tau st'+          (# (# #) | #)        -> (# | (# reverse acc, st #) #)++    -- | Negative lookahead at a single character: a peek, with no state built.+    notCharStep :: (Char -> Bool) -> Step s ()+    notCharStep p = go+      where+        go tau st+          | plainly tau st = case uncons (stInput st) of+              (# | (# x, _ #) #) | p x -> (# (# #) | #)+              _                        -> (# | (# (), st #) #)+          | otherwise = case satStep p tau st of+              (# (# #) | #) -> (# | (# (), st #) #)+              _             -> (# (# #) | #)++--------------------------------------------------------------------------------+-- Column bookkeeping+--------------------------------------------------------------------------------++-- | Consume the head character, updating column, offset and the candidate+-- interval.+--+-- When the ambient relation is total ('rdTotal', i.e. 'anyR') and no+-- alignment is pending, the candidate interval is provably unchanged, so the+-- whole interval computation is skipped.  Grammars that do not use layout+-- take this branch for every single character.+advance :: Int -> RelD -> PState s -> Char -> s -> Res s Char+advance tw tau (PState _ col off cands aligned) x xs+  | aligned =+      if memberI col cands+        then (# | (# x, PState xs col' off' (singletonI col) False #) #)+        else (# (# #) | #)+  | rdTotal tau =+      if nullI cands+        then (# (# #) | #)+        else (# | (# x, PState xs col' off' cands False #) #)+  | memberI col (preimage tau cands) =+      (# | (# x, PState xs col' off'+                        (interI cands (image tau (singletonI col))) False #) #)+  | otherwise = (# (# #) | #)   where-    loop st = case step st of-      Nothing       -> ([], st)-      Just (x, st') -> let (xs, rest) = loop st' in (x : xs, rest)+    !col' = nextCol tw col x+    !off' = off + 1+{-# INLINE advance #-} -ruleFor :: Member s defs ty a -> Rules env defs -> PExp env ty a-ruleFor Here      (RCons _ body _)    = body-ruleFor (There m) (RCons _ _    rest) = ruleFor m rest+-- | Does the cheap path apply?  It does when the ambient relation constrains+-- nothing, no alignment is pending, and the candidate interval is inhabited:+-- under those conditions 'advance' provably leaves the interval alone, so a+-- run of characters can be consumed without touching it once.+plainly :: RelD -> PState s -> Bool+plainly tau st = rdTotal tau && not (stAlign st) && not (nullI (stCands st))+{-# INLINE plainly #-}
src/PEG/QQ.hs view
@@ -15,8 +15,13 @@ -- a Haskell action in braces: @{ haskellExpr }@. -- Ordered choice is written with @\/@; Kleene star with @*@; plus with @+@; -- optional with @?@; negation with @!@.--- Character classes use @[...]@ syntax. --+-- Character classes use @[...]@ syntax and may contain ranges: @[a-zA-Z0-9_]@.+-- A leading @^@ negates the class, so @[^\"]@ matches any character other than+-- a double quote; write @[\\^]@ for a class containing a caret.  Prefer a+-- negated class over the @(!c .)@ idiom: the class is one bit test, whereas+-- the lookahead scans every character twice.+-- -- The 'pegExpr' quasi-quoter produces a single 'PEG.Syntax.PExp' value, -- while 'pegRules' produces a complete set of named rules (a -- 'PEG.Grammar.Rules' value) to be passed to 'PEG.Grammar.Grammar'.@@ -50,7 +55,7 @@   | EPlus    PExpr   | EChar    Char   | EString  String-  | EClass   [(Char,Char)]+  | EClass   Bool [(Char,Char)]   -- ^ 'True' when the class is negated.   | EDot   | ENT      String   | EIndent  RelS PExpr@@ -127,16 +132,26 @@   '['  -> Right ('[',  xs)   ']'  -> Right (']',  xs)   '0'  -> Right ('\0', xs)+  '^'  -> Right ('^',  xs)   _    -> errorAt ("unknown escape \\" ++ [e]) xs escChar stopC (c:xs)   | c == stopC = errorAt "unexpected close quote" (c:xs)   | otherwise  = Right (c, xs) escChar _ [] = Left "unexpected end of input in literal" -classLit :: P [(Char, Char)]+-- | A character class.  A leading @^@ negates it, as in POSIX; write+-- @[\\^]@ for a class containing the caret itself.+classLit :: P (Bool, [(Char, Char)]) classLit s0 = case spaces s0 of-  ('[':xs) -> loop xs-  s        -> errorAt "expected character class" s+  ('[':'^':xs) -> do+    (rs, r) <- loop xs+    if null rs+      then errorAt "empty negated character class" s0+      else Right ((True, rs), r)+  ('[':xs)     -> do+    (rs, r) <- loop xs+    Right ((False, rs), r)+  s            -> errorAt "expected character class" s   where     loop (']':r) = Right ([], r)     loop []      = Left "unterminated character class"@@ -302,7 +317,7 @@         Left _ -> case strLit s of           Right (cs, s1) -> Right (EString cs, s1)           Left _ -> case classLit s of-            Right (rs, s1) -> Right (EClass rs, s1)+            Right ((neg, rs), s1) -> Right (EClass neg rs, s1)             Left _ -> case ident s of               Right (name, s1) ->                 case tok "<-" s1 of@@ -344,9 +359,13 @@ translateExpr (EString s)   | null s    = [| pureP "" |]   | otherwise = [| stringNE s |]-translateExpr (EClass rs) =-  let allChars = concat [ [lo..hi] | (lo, hi) <- rs ]-  in [| oneOf allChars |]+translateExpr (EClass neg rs)+  -- A character class becomes a single 'Sat' node holding a compact+  -- 'PEG.CharSet.CharSet'.  Expanding it into a chain of ordered choices, as+  -- an earlier version did, made matching one character of @[a-zA-Z0-9_]@+  -- cost 63 parser steps.+  | neg       = [| notCharClass rs |]+  | otherwise = [| charClass rs |] translateExpr (EAnd e)  = do   e' <- translateExpr e   [| Not (Not $(pure e')) |]@@ -356,6 +375,21 @@ translateExpr (EOpt e)  = do   e' <- translateExpr e   [| opt $(pure e') |]+-- A repetition of a single character -- @[a-z]*@, @','+@, @.*@ -- compiles to+-- one 'PEG.Syntax.Span' node and produces a /chunk of the input stream/: a+-- 'Data.Text.Text' slice rather than a @['Char']@.  Only a bare class, literal+-- or dot qualifies; a wrapper such as @[a-z]^>*@ changes the meaning of each+-- iteration, so those keep the generic 'Star'.+translateExpr (EStar (EClass neg rs))+  | neg       = [| spanOf (notInRanges rs) |]+  | otherwise = [| spanOf (fromRanges rs) |]+translateExpr (EStar (EChar c)) = [| spanOf (singletonCS c) |]+translateExpr (EStar EDot)      = [| spanOf anyCS |]+translateExpr (EPlus (EClass neg rs))+  | neg       = [| spanOf1 (notInRanges rs) |]+  | otherwise = [| spanOf1 (fromRanges rs) |]+translateExpr (EPlus (EChar c)) = [| spanOf1 (singletonCS c) |]+translateExpr (EPlus EDot)      = [| spanOf1 anyCS |] translateExpr (EStar e) = do   e' <- translateExpr e   [| Star $(pure e') |]
src/PEG/Semantics/Simple.hs view
@@ -73,17 +73,19 @@ p </> q = try p <|> q  -class Stream d where+-- | Unrelated to "PEG.Stream": this is the reference semantics' own+-- token-polymorphic input class, used only inside this module.+class SimpleStream d where   type Elem d   anyChar :: PExp d (Elem d) -instance Stream [a] where+instance SimpleStream [a] where   type Elem [a] = a    anyChar = PExp $ \s -> case s of     (x:xs) -> Commit xs x     [] -> Fail "EOF" False -satisfy :: Stream d => (Elem d -> Bool) -> PExp d (Elem d)+satisfy :: SimpleStream d => (Elem d -> Bool) -> PExp d (Elem d) satisfy p = try $ do   x <- anyChar   x <$ guard (p x)@@ -101,10 +103,10 @@         Fail{} -> Pure ()         _      -> Fail "unexpected" False -eof :: Stream d => PExp d ()+eof :: SimpleStream d => PExp d () eof = not anyChar -char :: Eq (Elem d) => Stream d => Elem d -> PExp d (Elem d)+char :: Eq (Elem d) => SimpleStream d => Elem d -> PExp d (Elem d) char c = satisfy (c ==)  lexeme :: PExp String a -> PExp String a
+ src/PEG/Stream.hs view
@@ -0,0 +1,263 @@+{-# LANGUAGE BangPatterns      #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MagicHash         #-}+{-# LANGUAGE UnboxedSums       #-}+{-# LANGUAGE UnboxedTuples     #-}++-- | Input streams the parser can consume.+--+-- A 'Stream' is anything the parser can read one 'Char' at a time and slice+-- chunks out of.  Instances are provided for 'String', strict and lazy+-- 'Data.Text.Text', and strict and lazy 'Data.ByteString.ByteString'.+--+-- == ByteString is Latin-1+--+-- The 'ByteString' instances read each byte as the 'Char' with that code+-- point, exactly as "Data.ByteString.Char8" does.  This is what makes them+-- fast — every character lands in the Latin-1 range that+-- "PEG.CharSet" answers with a single bit test — and it is correct for+-- grammars over ASCII or Latin-1 text.  It is /wrong/ for UTF-8: a+-- multi-byte character arrives as its individual bytes, and columns and+-- offsets count bytes rather than characters.  Decode to 'Data.Text.Text'+-- first if that matters.+--+-- Two laws follow, and only the 'ByteString' instances need the caveat:+--+-- * @'chunkToString' . 'packString' == 'id'@, for arguments in the range the+--   stream can represent (all of 'Char' except for 'ByteString', where it is+--   @\'\\0\'@ .. @\'\\255\'@).+-- * A 'PEG.CharSet.CharSet' containing only characters above @\'\\255\'@+--   never matches a 'ByteString', with no diagnostic.+--+-- == Writing an instance+--+-- Only 'unconsS' has no default.  Everything else is derived from it, so a+-- minimal instance is one method — but a type with native slicing should+-- override 'spanS', 'takeS', 'lengthS' and 'foldlS'' , which is where the+-- performance of 'Text' and 'ByteString' comes from.+module PEG.Stream+  ( Stream (..)+  ) where++import Data.Char (chr, ord)++import qualified Data.ByteString            as B+import qualified Data.ByteString.Char8      as BC+import qualified Data.ByteString.Lazy       as BL+import qualified Data.ByteString.Lazy.Char8 as BLC+import qualified Data.Text                  as T+import qualified Data.Text.Lazy             as TL++-- | A sequence of characters the parser can consume.+--+-- The chunk type is the stream type itself: slicing a 'Data.Text.Text'+-- yields a 'Data.Text.Text', so a character class such as @[a-z]+@ produces+-- a real slice rather than unpacking into a @['Char']@.+class Stream s where+  -- | Split off the first character.+  --+  -- This returns an unboxed sum rather than @'Maybe' ('Char', s)@ on+  -- purpose.  It is called once per character of input, and the boxed+  -- version would allocate a @Just@ and a pair every time — behind a class+  -- dictionary GHC cannot cancel them, so the parser's zero-allocation+  -- terminal path would be lost.+  unconsS :: s -> (# (# #) | (# Char, s #) #)++  -- | @'spanS' p s@ splits @s@ into the longest prefix all of whose+  -- characters satisfy @p@, and the rest.+  spanS :: (Char -> Bool) -> s -> (s, s)++  -- | Strict left fold over the characters.  Used to advance the column+  -- across a chunk that has already been matched in bulk.+  foldlS' :: (b -> Char -> b) -> b -> s -> b++  -- | Prepend a character.  @O(1)@ for 'String' and the lazy types; the+  -- strict types must copy.+  consS :: Char -> s -> s++  -- | @'takeS' n s@ is the first @n@ characters of @s@.+  takeS :: Int -> s -> s++  -- | Number of characters.+  lengthS :: s -> Int++  -- | Is the stream empty?+  nullS :: s -> Bool++  -- | Convert a chunk to a 'String'.  Semantic actions need this whenever a+  -- character class feeds something that expects a 'String', such as 'read'.+  chunkToString :: s -> String++  -- | Build a chunk from a 'String'.+  packString :: String -> s++  -- Defaults, all in terms of 'unconsS'.++  spanS p s0 = go id s0+    where+      go acc s = case unconsS s of+        (# | (# c, s' #) #) | p c -> go (acc . (c :)) s'+        _                         -> (packString (acc []), s)++  foldlS' f = go+    where+      go !acc s = case unconsS s of+        (# | (# c, s' #) #) -> go (f acc c) s'+        _                   -> acc++  consS c s = packString (c : chunkToString s)++  takeS n0 s0 = packString (go n0 s0)+    where+      go n s+        | n <= 0    = []+        | otherwise = case unconsS s of+            (# | (# c, s' #) #) -> c : go (n - 1) s'+            _                   -> []++  lengthS = foldlS' (\ !n _ -> n + 1) 0++  nullS s = case unconsS s of+    (# (# #) | #) -> True+    _             -> False++  chunkToString s = case unconsS s of+    (# | (# c, s' #) #) -> c : chunkToString s'+    _                   -> []++  {-# MINIMAL unconsS, packString #-}++--------------------------------------------------------------------------------+-- String+--------------------------------------------------------------------------------++instance Stream [Char] where+  unconsS (c : cs) = (# | (# c, cs #) #)+  unconsS []       = (# (# #) | #)+  {-# INLINE unconsS #-}++  -- NOT 'Data.List.span': that one is lazy in its pair, so it allocates a+  -- tuple and two selector thunks for every character it accepts.  Finding+  -- the split point first and slicing costs one tuple in total.+  spanS p s0    = go (0 :: Int) s0+    where+      go !n s = case s of+        (c : cs) | p c -> go (n + 1) cs+        _              -> (take n s0, s)+  foldlS' f     = go+    where+      go !acc (c : cs) = go (f acc c) cs+      go !acc []       = acc+  consS         = (:)+  takeS         = take+  lengthS       = length+  nullS         = null+  chunkToString = id+  packString    = id+  {-# INLINE spanS #-}+  {-# INLINE foldlS' #-}+  {-# INLINE consS #-}+  {-# INLINE takeS #-}+  {-# INLINE lengthS #-}+  {-# INLINE nullS #-}+  {-# INLINE chunkToString #-}+  {-# INLINE packString #-}++--------------------------------------------------------------------------------+-- Text+--------------------------------------------------------------------------------++instance Stream T.Text where+  unconsS t = case T.uncons t of+    Just (c, t') -> (# | (# c, t' #) #)+    Nothing      -> (# (# #) | #)+  {-# INLINE unconsS #-}++  spanS         = T.span+  foldlS'       = T.foldl'+  consS         = T.cons+  takeS         = T.take+  lengthS       = T.length+  nullS         = T.null+  chunkToString = T.unpack+  packString    = T.pack+  {-# INLINE spanS #-}+  {-# INLINE foldlS' #-}+  {-# INLINE takeS #-}+  {-# INLINE lengthS #-}+  {-# INLINE nullS #-}++instance Stream TL.Text where+  unconsS t = case TL.uncons t of+    Just (c, t') -> (# | (# c, t' #) #)+    Nothing      -> (# (# #) | #)+  {-# INLINE unconsS #-}++  spanS         = TL.span+  foldlS'       = TL.foldl'+  consS         = TL.cons+  takeS n       = TL.take (fromIntegral n)+  lengthS       = fromIntegral . TL.length+  nullS         = TL.null+  chunkToString = TL.unpack+  packString    = TL.pack+  {-# INLINE spanS #-}+  {-# INLINE foldlS' #-}+  {-# INLINE nullS #-}++--------------------------------------------------------------------------------+-- ByteString (Latin-1)+--------------------------------------------------------------------------------++-- | Byte to character, Latin-1.+w2c :: Int -> Char+w2c = chr+{-# INLINE w2c #-}++instance Stream B.ByteString where+  unconsS b = case B.uncons b of+    Just (w, b') -> (# | (# w2c (fromIntegral w), b' #) #)+    Nothing      -> (# (# #) | #)+  {-# INLINE unconsS #-}++  spanS         = BC.span+  foldlS'       = BC.foldl'+  consS         = BC.cons+  takeS         = B.take+  lengthS       = B.length+  nullS         = B.null+  chunkToString = BC.unpack+  -- 'BC.pack' truncates characters above '\255'; clamp explicitly so the+  -- behaviour is the documented one rather than whatever pack happens to do.+  packString    = BC.pack . map clampLatin1+  {-# INLINE spanS #-}+  {-# INLINE foldlS' #-}+  {-# INLINE takeS #-}+  {-# INLINE lengthS #-}+  {-# INLINE nullS #-}++instance Stream BL.ByteString where+  unconsS b = case BL.uncons b of+    Just (w, b') -> (# | (# w2c (fromIntegral w), b' #) #)+    Nothing      -> (# (# #) | #)+  {-# INLINE unconsS #-}++  spanS         = BLC.span+  foldlS'       = BLC.foldl'+  consS         = BLC.cons+  takeS n       = BL.take (fromIntegral n)+  lengthS       = fromIntegral . BL.length+  nullS         = BL.null+  chunkToString = BLC.unpack+  packString    = BLC.pack . map clampLatin1+  {-# INLINE spanS #-}+  {-# INLINE foldlS' #-}+  {-# INLINE nullS #-}++-- | Characters a 'ByteString' cannot represent become @\'\\255\'@ rather+-- than silently wrapping around modulo 256.+clampLatin1 :: Char -> Char+clampLatin1 c+  | ord c > 255 = '\255'+  | otherwise   = c+{-# INLINE clampLatin1 #-}
src/PEG/Syntax.hs view
@@ -11,18 +11,28 @@  -- | The PEG expression GADT and combinator API. ----- 'PExp' is the core type: a GADT indexed by the grammar environment,--- the 'PEG.Type.Ty' of the expression (nullability + FIRST set), and the--- Haskell result type.  Combinators like '<*>.' and '.||.' propagate type--- information at the kind level so that 'PEG.Grammar.Acyclic' can be checked--- without running the parser.+-- 'PExp' is the core type: a GADT indexed by the input stream, the grammar+-- environment, the 'PEG.Type.Ty' of the expression (nullability + FIRST set),+-- and the Haskell result type.  Combinators like '<*>.' and '.||.' propagate+-- type information at the kind level so that 'PEG.Grammar.Acyclic' can be+-- checked without running the parser. --+-- The first parameter, @s@, is the stream the expression consumes; see+-- "PEG.Stream".  It appears in the type because a character class produces a+-- /chunk of that stream/ — matching @[a-z]+@ against a 'Data.Text.Text'+-- yields a 'Data.Text.Text' slice, not a @['Char']@.+-- -- Most users will not build 'PExp' values directly; instead they use the -- quasi-quoter in "PEG.QQ". module PEG.Syntax   ( Name (..)   , PExp (..)   , nt+  , sat+  , charClass+  , notCharClass+  , spanOf+  , spanOf1   , pureP   , fmapP   , indent@@ -39,20 +49,36 @@   , SeqTy   , ChoiceTy   , NTTy+  , NTGo   ) where  import Data.Kind    (Type) import GHC.TypeLits (Symbol, KnownSymbol) +import PEG.CharSet (CharSet)+import qualified PEG.CharSet as CS import PEG.Indent (Rel) import PEG.Type import PEG.TyLevel import PEG.Member --- | A singleton witness for a non-terminal name @s@.-data Name (s :: Symbol) = Name+-- | A singleton witness for a non-terminal name @n@.+data Name (n :: Symbol) = Name  -- | The 'Ty' of a sequence @e1 e2@.+--+-- Written as a projective type synonym rather than a type family so that it+-- reduces to a @'MkTy'@ head even when its operands are still abstract.  That+-- is what lets a polymorphic combinator such as+--+-- @+-- lexeme :: PExp s env ty a -> PExp s env (SeqTy ty ('MkTy 'True '[])) a+-- @+--+-- compose without the caller having to get the nesting of 'SeqTy' exactly+-- right.  The cost it used to carry — an exponential blow-up as the operands+-- get duplicated across the right-hand side — came from 'Union' and+-- 'ConsIfAbsent', not from here; see "PEG.TyLevel". type SeqTy t1 t2 =   'MkTy (And (Nullable t1) (Nullable t2))         (Union (First t1) (If (Nullable t1) (First t2) '[]))@@ -62,17 +88,30 @@   'MkTy (Or  (Nullable t1) (Nullable t2))         (Union (First t1) (First t2)) --- | The 'Ty' of a non-terminal reference @s@ looked up in @env@.-type NTTy s env =-  'MkTy (Nullable (TyOf (Lookup s env)))-        (ConsIfAbsent s (First (TyOf (Lookup s env))))+-- | The 'Ty' of a non-terminal reference @n@ looked up in @env@.+type NTTy n env = NTGo n (TyOf (Lookup n env)) --- | A typed PEG expression.+-- | The 'Ty' of a reference to a non-terminal named @n@ whose own 'Ty' is+-- @t@. --+-- 'NT' and 'nt' are stated in terms of this rather than 'NTTy' so that the+-- environment is searched /once/ per occurrence, by the constructor's+-- @Lookup n env ~ 'EnvEntry ty a@ equality.  Naming @Lookup n env@ twice, as+-- an expansion of 'NTTy' does, doubles the cost of what profiling shows to be+-- the dominant term in checking a large grammar.+type family NTGo (n :: Symbol) (t :: Ty) :: Ty where+  NTGo n ('MkTy nu f) = 'MkTy nu (ConsIfAbsent n f)++-- | A typed PEG expression over the stream @s@.+-- -- Constructors correspond to the standard PEG operators: -- -- * 'Pure'   — succeed without consuming input, return a value -- * 'Term'   — match a specific character+-- * 'Sat'    — match any character of a 'CharSet' (a character class)+-- * 'Str'    — match a non-empty string literal+-- * 'Span'   — match a run of characters of a 'CharSet', possibly empty+-- * 'Span1'  — match a non-empty run of characters of a 'CharSet' -- * 'AnyChar'— match any character -- * 'NT'     — invoke a named non-terminal -- * 'Seq'    — sequential composition (@e1 e2@)@@ -83,111 +122,165 @@ -- * 'Indent' — require the next token to satisfy an indentation relation -- * 'Position'— set the column relation for tokens inside the sub-expression -- * 'Align'  — require the next token to be aligned with the current position-data PExp (env :: Env) (ty :: Ty) (a :: Type) where-  Pure     :: a -> PExp env ('MkTy 'True '[]) a-  Term     :: Char -> PExp env ('MkTy 'False '[]) Char-  AnyChar  :: PExp env ('MkTy 'False '[]) Char-  NT       :: ( KnownSymbol s-              , KnownMember s env (TyOf (Lookup s env)) (ResOf (Lookup s env))+data PExp (s :: Type) (env :: Env) (ty :: Ty) (a :: Type) where+  Pure     :: a -> PExp s env ('MkTy 'True '[]) a+  Term     :: Char -> PExp s env ('MkTy 'False '[]) Char+  -- | Match one character of a class.  This is what a character class such as+  -- @[a-zA-Z0-9_]@ compiles to: a single bit test instead of a chain of+  -- ordered choices.+  Sat      :: !CharSet -> PExp s env ('MkTy 'False '[]) Char+  -- | Match a string literal.  The string must be non-empty (the 'Ty' index+  -- claims the expression is not nullable); use 'pureP' @""@ otherwise.+  --+  -- The result is the literal itself, so it is shared rather than sliced out+  -- of the input.+  Str      :: String -> PExp s env ('MkTy 'False '[]) String+  -- | Match the longest run of characters belonging to a class, possibly+  -- empty — what @[a-z]*@ compiles to.  The result is a chunk of the input+  -- stream, so on 'Data.Text.Text' this is a slice and costs no copy.+  Span     :: !CharSet -> PExp s env ('MkTy 'True  '[]) s+  -- | As 'Span', but the run must be non-empty: @[a-z]+@.+  Span1    :: !CharSet -> PExp s env ('MkTy 'False '[]) s+  AnyChar  :: PExp s env ('MkTy 'False '[]) Char+  -- The environment is looked up /once/, by the equality below, and the+  -- result is bound to the rigid variables @ty@ and @a@.  Passing+  -- @TyOf (Lookup n env)@ straight to 'KnownMember' instead makes GHC+  -- re-reduce the lookup at every step of the instance chain that walks+  -- @env@, which costs @O(|env|^2)@ per non-terminal occurrence.+  NT       :: forall n ty s env a.+              ( KnownSymbol n+              , Lookup n env ~ 'EnvEntry ty a+              , KnownMember n env a               )-           => Name s-           -> PExp env (NTTy s env) (ResOf (Lookup s env))-  Seq      :: PExp env t1 (a -> b)-           -> PExp env t2 a-           -> PExp env (SeqTy t1 t2) b-  Choice   :: PExp env t1 a-           -> PExp env t2 a-           -> PExp env (ChoiceTy t1 t2) a-  Star     :: PExp env ('MkTy 'False f) a-           -> PExp env ('MkTy 'True  f) [a]-  Not      :: PExp env ('MkTy n f) a-           -> PExp env ('MkTy 'True f) ()+           => Name n+           -> PExp s env (NTGo n ty) a+  Seq      :: PExp s env t1 (a -> b)+           -> PExp s env t2 a+           -> PExp s env (SeqTy t1 t2) b+  Choice   :: PExp s env t1 a+           -> PExp s env t2 a+           -> PExp s env (ChoiceTy t1 t2) a+  Star     :: PExp s env ('MkTy 'False f) a+           -> PExp s env ('MkTy 'True  f) [a]+  Not      :: PExp s env ('MkTy n f) a+           -> PExp s env ('MkTy 'True f) ()   Map      :: (a -> b)-           -> PExp env ty a-           -> PExp env ty b+           -> PExp s env ty a+           -> PExp s env ty b   Indent   :: Rel n-           -> PExp env ty a-           -> PExp env ty a+           -> PExp s env ty a+           -> PExp s env ty a   Position :: Rel n-           -> PExp env ty a-           -> PExp env ty a-  Align    :: PExp env ty a-           -> PExp env ty a+           -> PExp s env ty a+           -> PExp s env ty a+  Align    :: PExp s env ty a+           -> PExp s env ty a -instance Functor (PExp env ty) where+instance Functor (PExp s env ty) where   fmap = Map  -- | Reference a non-terminal by name using a type application: -- @nt \@\"ruleName\"@.-nt :: forall s env.-      ( KnownSymbol s-      , KnownMember s env (TyOf (Lookup s env)) (ResOf (Lookup s env))+--+-- The name is deliberately the /first/ quantified variable, so that+-- @nt \@\"expr\"@ keeps working: the stream and environment are recovered by+-- unification.+nt :: forall n env s ty a.+      ( KnownSymbol n+      , Lookup n env ~ 'EnvEntry ty a+      , KnownMember n env a       )-   => PExp env (NTTy s env) (ResOf (Lookup s env))-nt = NT (Name :: Name s)+   => PExp s env (NTGo n ty) a+nt = NT (Name :: Name n)  -- | Succeed without consuming any input.-pureP :: a -> PExp env ('MkTy 'True '[]) a+pureP :: a -> PExp s env ('MkTy 'True '[]) a pureP = Pure  -- | Apply a function to the result of an expression.-fmapP :: (a -> b) -> PExp env ty a -> PExp env ty b+fmapP :: (a -> b) -> PExp s env ty a -> PExp s env ty b fmapP = Map  -- | Require the sub-expression to satisfy the given column relation.-indent :: Rel n -> PExp env ty a -> PExp env ty a+indent :: Rel n -> PExp s env ty a -> PExp s env ty a indent = Indent  -- | Override the token mode for the sub-expression.-position :: Rel n -> PExp env ty a -> PExp env ty a+position :: Rel n -> PExp s env ty a -> PExp s env ty a position = Position  -- | Require the sub-expression to start at the current alignment column.-align :: PExp env ty a -> PExp env ty a+align :: PExp s env ty a -> PExp s env ty a align = Align  -- | Infix synonym for 'fmapP'.-(<$>.) :: (a -> b) -> PExp env ty a -> PExp env ty b+(<$>.) :: (a -> b) -> PExp s env ty a -> PExp s env ty b (<$>.) = Map infixl 4 <$>.  -- | Infix sequential composition.-(<*>.) :: PExp env t1 (a -> b)-       -> PExp env t2 a-       -> PExp env (SeqTy t1 t2) b+(<*>.) :: PExp s env t1 (a -> b)+       -> PExp s env t2 a+       -> PExp s env (SeqTy t1 t2) b (<*>.) = Seq infixl 4 <*>.  -- | Sequence two expressions, discarding the result of the first.-(.>>.) :: PExp env t1 a-       -> PExp env t2 b-       -> PExp env (SeqTy t1 t2) b+(.>>.) :: PExp s env t1 a+       -> PExp s env t2 b+       -> PExp s env (SeqTy t1 t2) b e1 .>>. e2 = Map (\_ b -> b) e1 <*>. e2 infixl 6 .>>.  -- | Infix ordered choice (@e1 \/ e2@): try @e1@; if it fails, try @e2@.-(.||.) :: PExp env t1 a -> PExp env t2 a -> PExp env (ChoiceTy t1 t2) a+(.||.) :: PExp s env t1 a -> PExp s env t2 a -> PExp s env (ChoiceTy t1 t2) a (.||.) = Choice infixl 5 .||.  -- | Optional match: @opt e = (Just \<$\>. e) .||. pureP Nothing@.-opt :: PExp env t a-    -> PExp env (ChoiceTy t ('MkTy 'True '[])) (Maybe a)+opt :: PExp s env t a+    -> PExp s env (ChoiceTy t ('MkTy 'True '[])) (Maybe a) opt e = (Just <$>. e) .||. pureP Nothing  -- | One-or-more: @plus e = (:) \<$\>. e \<*\>. Star e@.-plus :: PExp env ('MkTy 'False f) a-     -> PExp env (SeqTy ('MkTy 'False f) ('MkTy 'True f)) [a]+--+-- For a single character class, prefer 'spanOf1': it matches the whole run in+-- one scan and returns a chunk of the stream instead of a list.+plus :: PExp s env ('MkTy 'False f) a+     -> PExp s env (SeqTy ('MkTy 'False f) ('MkTy 'True f)) [a] plus e = (:) <$>. e <*>. Star e +-- | Match any character of the given set.+sat :: CharSet -> PExp s env ('MkTy 'False '[]) Char+sat = Sat++-- | Match any character inside one of the given inclusive ranges.+-- This is the representation the quasi-quoter emits for @[a-z0-9]@ and+-- friends.+charClass :: [(Char, Char)] -> PExp s env ('MkTy 'False '[]) Char+charClass = Sat . CS.fromRanges++-- | Match any character /outside/ the given inclusive ranges.+-- The quasi-quoter emits this for @[^\"]@.+notCharClass :: [(Char, Char)] -> PExp s env ('MkTy 'False '[]) Char+notCharClass = Sat . CS.notInRanges++-- | Match the longest run of characters of the set, possibly empty.  The+-- result is a chunk of the input stream.+spanOf :: CharSet -> PExp s env ('MkTy 'True '[]) s+spanOf = Span++-- | Match a non-empty run of characters of the set.+spanOf1 :: CharSet -> PExp s env ('MkTy 'False '[]) s+spanOf1 = Span1+ -- | Match any character in the given list. The list must be non-empty.-oneOf :: [Char] -> PExp env ('MkTy 'False '[]) Char-oneOf []     = error "PEG.Syntax.oneOf: empty character class"-oneOf [c]    = Term c-oneOf (c:cs) = Term c .||. oneOf cs+oneOf :: [Char] -> PExp s env ('MkTy 'False '[]) Char+oneOf []  = error "PEG.Syntax.oneOf: empty character class"+oneOf [c] = Term c+oneOf cs  = Sat (CS.fromList cs)  -- | Match an exact string literal. The string must be non-empty.-stringNE :: String -> PExp env ('MkTy 'False '[]) String-stringNE []     = error "PEG.Syntax.stringNE: empty string"-stringNE [c]    = (\x -> [x]) <$>. Term c-stringNE (c:cs) = (:) <$>. Term c <*>. stringNE cs+stringNE :: String -> PExp s env ('MkTy 'False '[]) String+stringNE [] = error "PEG.Syntax.stringNE: empty string"+stringNE s  = Str s
src/PEG/TyLevel.hs view
@@ -10,6 +10,30 @@ -- These type families are used internally to compute the FIRST sets and -- nullability of PEG expressions at the kind level, enabling the -- 'PEG.Grammar.Acyclic' constraint to be resolved at compile time.+--+-- == Representation of FIRST sets+--+-- A FIRST set is a @['Symbol']@ kept /strictly sorted/ by 'CmpSymbol'.+-- Sortedness is the whole point: it makes the representation canonical (one+-- set, one type), so 'Union' is a single-pass merge and 'Elem' can stop at+-- the first symbol greater than the one it is looking for.+--+-- == Why the families are written this way+--+-- Every clause below mentions each of its arguments — and in particular each+-- recursive call — /exactly once/ on the right-hand side.  This is not a+-- style choice.  A clause such as+--+-- @+-- ConsIfAbsent x xs = If (Elem x xs) xs (x ': xs)   -- DON'T+-- @+--+-- mentions @xs@ three times, and @xs@ is normally an unreduced application+-- of 'Union'.  GHC therefore has three copies of the pending computation to+-- reduce, each of which triples again one level down: a union of two sets of+-- size @n@ costs @3^n@ reductions rather than @n@.  Dispatching on an+-- already-computed 'Ordering' in a separate family keeps every right-hand+-- side linear in its arguments. module PEG.TyLevel   ( If   , And@@ -45,30 +69,83 @@   IsEQ 'EQ = 'True   IsEQ _   = 'False +-- | Is @x@ a member of the sorted set @xs@?+--+-- Stops as soon as it reaches a symbol greater than @x@, so a miss costs+-- half a scan on average rather than a full one. type family Elem (x :: Symbol) (xs :: [Symbol]) :: Bool where   Elem _ '[]       = 'False-  Elem x (y ': ys) = Or (SymEq x y) (Elem x ys)+  Elem x (y ': ys) = ElemGo (CmpSymbol x y) x ys +type family ElemGo (o :: Ordering) (x :: Symbol) (ys :: [Symbol]) :: Bool where+  ElemGo 'EQ _ _  = 'True+  ElemGo 'LT _ _  = 'False+  ElemGo 'GT x ys = Elem x ys++-- | Insert @x@ into the sorted set @xs@, keeping it sorted and duplicate-free. type family ConsIfAbsent (x :: Symbol) (xs :: [Symbol]) :: [Symbol] where-  ConsIfAbsent x xs = If (Elem x xs) xs (x ': xs)+  ConsIfAbsent x '[]       = '[x]+  ConsIfAbsent x (y ': ys) = InsGo (CmpSymbol x y) x y ys +type family InsGo (o :: Ordering) (x :: Symbol) (y :: Symbol)+                  (ys :: [Symbol]) :: [Symbol] where+  InsGo 'LT x y ys = x ': y ': ys+  InsGo 'EQ _ y ys = y ': ys+  InsGo 'GT x y ys = y ': ConsIfAbsent x ys++-- | Union of two sorted sets: a single merge pass, @O(|xs| + |ys|)@.+--+-- The merge nests one type-family reduction per element of the result, so a+-- FIRST set of more than about a hundred non-terminals runs into GHC's+-- default reduction limit and reports @Reduction stack overflow@.  That is a+-- limit, not a slowdown: @-freduction-depth=0@ lifts it, and a union of two+-- 128-element sets then takes about 0.3 s. type family Union (xs :: [Symbol]) (ys :: [Symbol]) :: [Symbol] where-  Union '[]       ys = ys-  Union (x ': xs) ys = ConsIfAbsent x (Union xs ys)+  Union '[]       ys        = ys+  Union (x ': xs) '[]       = x ': xs+  Union (x ': xs) (y ': ys) = MergeGo (CmpSymbol x y) x xs y ys +type family MergeGo (o :: Ordering) (x :: Symbol) (xs :: [Symbol])+                    (y :: Symbol) (ys :: [Symbol]) :: [Symbol] where+  MergeGo 'LT x xs y ys = x ': Union xs (y ': ys)+  MergeGo 'EQ x xs _ ys = x ': Union xs ys+  MergeGo 'GT x xs y ys = y ': Union (x ': xs) ys++-- | Look up a non-terminal's entry in the environment.+--+-- This is the hot path: there is one lookup per occurrence of every+-- non-terminal in the grammar, so it is written to do as little as possible+-- per entry scanned.+--+-- Two things matter.  The search proper ('LookupMb') carries only the tail it+-- still has to scan — threading the /whole/ environment through it so the+-- not-found case could name the available non-terminals costs a traversal of+-- that environment at every step, and an environment of @n@ rules is itself+-- @O(n^2)@ type nodes because every rule carries a FIRST set.  The+-- environment is therefore named once, in 'Found', which only reduces after+-- the search has finished.+--+-- And the match is on a /non-linear/ pattern — @s@ appears twice in the+-- second clause — rather than on @CmpSymbol s t@ dispatched through a helper+-- family.  GHC decides the clause by syntactic equality and by apartness for+-- the fall-through, which is one type-family reduction per entry instead of+-- two.  (The trick is @Data.Type.Map@'s, from @type-level-sets@.)  It costs+-- nothing here: unlike 'Elem', this search has no sortedness to exploit, so+-- there was never a third case to short-circuit on. type family Lookup (s :: Symbol) (env :: Env) :: EnvEntry where-  Lookup s env = LookupGo s env env+  Lookup s env = Found s env (LookupMb s env) -type family LookupGo (s :: Symbol) (env :: Env) (full :: Env) :: EnvEntry where-  LookupGo s '[] full =-    TypeError ('Text "Undefined non-terminal: " ':<>: 'ShowType s-         ':$$: 'Text "Available non-terminals: " ':<>: 'ShowType (Names full))-  LookupGo s ('(t, e) ': rest) full = LookupStep (SymEq s t) s e rest full+type family LookupMb (s :: Symbol) (env :: Env) :: Maybe EnvEntry where+  LookupMb _ '[]               = 'Nothing+  LookupMb s ('(s, e) ': rest) = 'Just e+  LookupMb s (_ ': rest)       = LookupMb s rest -type family LookupStep (b :: Bool) (s :: Symbol) (e :: EnvEntry)-                       (rest :: Env) (full :: Env) :: EnvEntry where-  LookupStep 'True  _ e _    _    = e-  LookupStep 'False s _ rest full = LookupGo s rest full+type family Found (s :: Symbol) (env :: Env)+                  (r :: Maybe EnvEntry) :: EnvEntry where+  Found _ _ ('Just e) = e+  Found s env 'Nothing =+    TypeError ('Text "Undefined non-terminal: " ':<>: 'ShowType s+         ':$$: 'Text "Available non-terminals: " ':<>: 'ShowType (Names env))  type family Names (env :: Env) :: [Symbol] where   Names '[]               = '[]
typed-peg.cabal view
@@ -1,16 +1,21 @@ cabal-version:      3.0 name:               typed-peg-version:            0.1.0.0+version:            0.2.0.0 synopsis:           Type-safe PEG parser combinators description:-  A library for building PEG (Parsing Expression Grammar) parsers+  A library for building Parsing Expression Grammars parsers   with compile-time safety guarantees. Grammar non-terminals are   indexed by their nullability and FIRST sets at the type level,   making left-recursive grammars a type error.   .-  A quasi-quoter ('PEG.QQ') allows writing grammars in a concrete+  A quasi-quoter (@PEG.QQ@) allows writing grammars in a concrete   DSL syntax. Indentation-sensitive parsing is supported natively-  via 'PEG.Indent'.+  via @PEG.Indent@.+  .+  Parsers run over any @PEG.Stream@ instance: @String@, strict and+  lazy @Text@, and strict and lazy @ByteString@. A character class+  produces a chunk of the input stream, so matching @[a-z]+@ against+  a @Text@ yields a slice rather than a @[Char]@.  license:            BSD-3-Clause license-file:       LICENSE@@ -21,8 +26,10 @@ bug-reports:        https://github.com/rodrigogribeiro/typed-peg/issues build-type:         Simple extra-source-files: README.md-extra-doc-files:    CHANGELOG.md-tested-with:        GHC == 9.6.7+extra-doc-files:+  CHANGELOG.md+  peg-patterns.md+tested-with:        GHC == 9.10.3  source-repository head   type:     git@@ -52,6 +59,7 @@   hs-source-dirs:  src   exposed-modules:     PEG+    PEG.CharSet     PEG.Grammar     PEG.Indent     PEG.Member@@ -59,19 +67,43 @@     PEG.QQ     PEG.QQ.HsExp     PEG.Semantics.Simple+    PEG.Stream     PEG.Syntax     PEG.TyLevel     PEG.Type   build-depends:       base             >= 4.18 && < 5-    , template-haskell >= 2.19 && < 2.22+    , bytestring       >= 0.11 && < 0.13+    , template-haskell >= 2.19 && < 2.24+    , text             >= 2.0  && < 2.2  test-suite typed-peg-examples   import:          common-opts   type:            exitcode-stdio-1.0   hs-source-dirs:  examples   main-is:         Main.hs-  other-modules:   Arith, Layout+  other-modules:   Arith, Layout, Compat, Patterns   build-depends:       base+    , bytestring+    , text     , typed-peg++benchmark typed-peg-bench+  import:          common-opts+  type:            exitcode-stdio-1.0+  hs-source-dirs:  bench+  main-is:         Main.hs+  other-modules:+    Bench.Inputs+    Bench.Peg+    Bench.Mega+  ghc-options:     -O2 -rtsopts "-with-rtsopts=-T"+  build-depends:+      base+    , typed-peg+    , bytestring+    , criterion    >= 1.6 && < 1.7+    , megaparsec   >= 9.5 && < 10+    , deepseq+    , text