typed-peg 0.3.0.0 → 0.4.0.0
raw patch · 22 files changed
+1508/−373 lines, 22 filesdep +containersPVP ok
version bump matches the API change (PVP)
Dependencies added: containers
API changes (from Hackage documentation)
- PEG.Analysis: instance GHC.Show.Show PEG.Analysis.Diagnostic
- PEG.Analysis: instance GHC.Show.Show PEG.Analysis.Ty
- PEG.Analysis: instance GHC.Show.Show PEG.Analysis.World
- PEG.CharSet: instance GHC.Show.Show PEG.CharSet.CharSet
- PEG.Indent: instance GHC.Show.Show (PEG.Indent.Rel n)
- PEG.Indent: instance GHC.Show.Show PEG.Indent.Bound
- PEG.Indent: instance GHC.Show.Show PEG.Indent.Interval
- 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: instance (GHC.Show.Show a, GHC.Show.Show s) => GHC.Show.Show (PEG.Parse.Result s a)
- PEG.QQ.HsExp: instance GHC.Show.Show PEG.QQ.HsExp.Tok
- PEG.QQ.Syntax: instance GHC.Show.Show PEG.QQ.Syntax.Def
- PEG.QQ.Syntax: instance GHC.Show.Show PEG.QQ.Syntax.Directive
- PEG.QQ.Syntax: instance GHC.Show.Show PEG.QQ.Syntax.Item
- PEG.QQ.Syntax: instance GHC.Show.Show PEG.QQ.Syntax.PExpr
- PEG.QQ.Syntax: instance GHC.Show.Show PEG.QQ.Syntax.RelS
- PEG.Semantics.Simple: instance GHC.Base.Alternative (PEG.Semantics.Simple.PExp d)
- PEG.Semantics.Simple: instance GHC.Base.Applicative (PEG.Semantics.Simple.PExp d)
- PEG.Semantics.Simple: instance GHC.Base.Functor (PEG.Semantics.Simple.PExp d)
- PEG.Semantics.Simple: instance GHC.Base.Functor (PEG.Semantics.Simple.Result d)
- PEG.Semantics.Simple: instance GHC.Base.Monad (PEG.Semantics.Simple.PExp d)
- PEG.Semantics.Simple: instance GHC.Base.MonadPlus (PEG.Semantics.Simple.PExp d)
- PEG.Syntax: [NTW] :: forall (n :: Symbol) s (env :: Env) a. (KnownSymbol n, Lookup n env ~ 'EnvEntry a) => Name n -> Member n env a -> PExp s env a
- PEG.Syntax: instance GHC.Base.Functor (PEG.Syntax.PExp s env)
+ PEG.Analysis: instance GHC.Internal.Show.Show PEG.Analysis.Diagnostic
+ PEG.Analysis: instance GHC.Internal.Show.Show PEG.Analysis.Ty
+ PEG.Analysis: instance GHC.Internal.Show.Show PEG.Analysis.World
+ PEG.CharSet: instance GHC.Internal.Show.Show PEG.CharSet.CharSet
+ PEG.Grammar: [Keyed] :: forall (nt :: Type -> Type) s a. Tabulate nt => (forall b. () => nt b -> PExp s nt b) -> PExp s nt a -> Grammar s nt a
+ PEG.Indent: instance GHC.Internal.Show.Show (PEG.Indent.Rel n)
+ PEG.Indent: instance GHC.Internal.Show.Show PEG.Indent.Bound
+ PEG.Indent: instance GHC.Internal.Show.Show PEG.Indent.Interval
+ PEG.Key: Table :: (forall b. () => nt b -> f b) -> Table (nt :: Type -> Type) (f :: Type -> Type)
+ PEG.Key: [InEnv] :: forall (n :: Symbol) (env :: Env) a. Member n env a -> InEnv env a
+ PEG.Key: [lookupTable] :: Table (nt :: Type -> Type) (f :: Type -> Type) -> forall b. () => nt b -> f b
+ PEG.Key: class Tabulate (nt :: Type -> Type)
+ PEG.Key: data InEnv (env :: Env) a
+ PEG.Key: newtype Table (nt :: Type -> Type) (f :: Type -> Type)
+ PEG.Key: ruleName :: Tabulate nt => nt b -> String
+ PEG.Key: tabulate :: Tabulate nt => (forall b. () => nt b -> f b) -> Table nt f
+ PEG.Member: instance PEG.Member.KnownMemberStep (GHC.Internal.TypeLits.Internal.CmpSymbol s t) s ('(t, e) : rest) a => PEG.Member.KnownMember s ('(t, e) : rest) a
+ PEG.Parse: instance (GHC.Internal.Show.Show a, GHC.Internal.Show.Show s) => GHC.Internal.Show.Show (PEG.Parse.Result s a)
+ PEG.QQ.HsExp: instance GHC.Internal.Show.Show PEG.QQ.HsExp.Tok
+ PEG.QQ.Syntax: instance GHC.Classes.Eq PEG.QQ.Syntax.Item
+ PEG.QQ.Syntax: instance GHC.Classes.Eq PEG.QQ.Syntax.PExpr
+ PEG.QQ.Syntax: instance GHC.Classes.Eq PEG.QQ.Syntax.RelS
+ PEG.QQ.Syntax: instance GHC.Internal.Show.Show PEG.QQ.Syntax.Def
+ PEG.QQ.Syntax: instance GHC.Internal.Show.Show PEG.QQ.Syntax.Directive
+ PEG.QQ.Syntax: instance GHC.Internal.Show.Show PEG.QQ.Syntax.Item
+ PEG.QQ.Syntax: instance GHC.Internal.Show.Show PEG.QQ.Syntax.PExpr
+ PEG.QQ.Syntax: instance GHC.Internal.Show.Show PEG.QQ.Syntax.RelS
+ PEG.Semantics.Simple: instance GHC.Internal.Base.Alternative (PEG.Semantics.Simple.PExp d)
+ PEG.Semantics.Simple: instance GHC.Internal.Base.Applicative (PEG.Semantics.Simple.PExp d)
+ PEG.Semantics.Simple: instance GHC.Internal.Base.Functor (PEG.Semantics.Simple.PExp d)
+ PEG.Semantics.Simple: instance GHC.Internal.Base.Functor (PEG.Semantics.Simple.Result d)
+ PEG.Semantics.Simple: instance GHC.Internal.Base.Monad (PEG.Semantics.Simple.PExp d)
+ PEG.Semantics.Simple: instance GHC.Internal.Base.MonadPlus (PEG.Semantics.Simple.PExp d)
+ PEG.Syntax: instance GHC.Internal.Base.Functor (PEG.Syntax.PExp s nt)
- PEG.Grammar: [Grammar] :: forall s (env :: Env) a. Rules s env env -> PExp s env a -> Grammar s env a
+ PEG.Grammar: [Grammar] :: forall s (env :: Env) a. Rules s env env -> PExp s (InEnv env) a -> Grammar s (InEnv env) a
- PEG.Grammar: [RCons] :: forall (n :: Symbol) s (env :: Env) a (rest :: [(Symbol, EnvEntry)]). Name n -> PExp s env a -> Rules s env rest -> Rules s env ('(n, 'EnvEntry a) ': rest)
+ PEG.Grammar: [RCons] :: forall (n :: Symbol) s (env :: Env) a (rest :: [(Symbol, EnvEntry)]). Name n -> PExp s (InEnv env) a -> Rules s env rest -> Rules s env ('(n, 'EnvEntry a) ': rest)
- PEG.Grammar: data Grammar s (env :: Env) a
+ PEG.Grammar: data Grammar s (nt :: Type -> Type) a
- PEG.Parse: compileGrammar :: forall s (env :: Env) a. Stream s => Int -> Grammar s env a -> Step s a
+ PEG.Parse: compileGrammar :: forall s (nt :: Type -> Type) a. Stream s => Int -> Grammar s nt a -> Step s a
- PEG.Parse: parse :: forall s (env :: Env) a. Stream s => Grammar s env a -> s -> Result s a
+ PEG.Parse: parse :: forall s (nt :: Type -> Type) a. Stream s => Grammar s nt a -> s -> Result s a
- PEG.Parse: parseWith :: forall s (env :: Env) a. Stream s => Opts -> Grammar s env a -> s -> Result s a
+ PEG.Parse: parseWith :: forall s (nt :: Type -> Type) a. Stream s => Opts -> Grammar s nt a -> s -> Result s a
- PEG.Syntax: (.>>.) :: forall s (env :: Env) a b. PExp s env a -> PExp s env b -> PExp s env b
+ PEG.Syntax: (.>>.) :: forall s (nt :: Type -> Type) a b. PExp s nt a -> PExp s nt b -> PExp s nt b
- PEG.Syntax: (.||.) :: forall s (env :: Env) a. PExp s env a -> PExp s env a -> PExp s env a
+ PEG.Syntax: (.||.) :: forall s (nt :: Type -> Type) a. PExp s nt a -> PExp s nt a -> PExp s nt a
- PEG.Syntax: (<$>.) :: forall a b s (env :: Env). (a -> b) -> PExp s env a -> PExp s env b
+ PEG.Syntax: (<$>.) :: forall a b s (nt :: Type -> Type). (a -> b) -> PExp s nt a -> PExp s nt b
- PEG.Syntax: (<*>.) :: forall s (env :: Env) a b. PExp s env (a -> b) -> PExp s env a -> PExp s env b
+ PEG.Syntax: (<*>.) :: forall s (nt :: Type -> Type) a b. PExp s nt (a -> b) -> PExp s nt a -> PExp s nt b
- PEG.Syntax: [Align] :: forall s (env :: Env) a. PExp s env a -> PExp s env a
+ PEG.Syntax: [Align] :: forall s (nt :: Type -> Type) a. PExp s nt a -> PExp s nt a
- PEG.Syntax: [AnyChar] :: forall s (env :: Env). PExp s env Char
+ PEG.Syntax: [AnyChar] :: forall s (nt :: Type -> Type). PExp s nt Char
- PEG.Syntax: [Choice] :: forall s (env :: Env) a. PExp s env a -> PExp s env a -> PExp s env a
+ PEG.Syntax: [Choice] :: forall s (nt :: Type -> Type) a. PExp s nt a -> PExp s nt a -> PExp s nt a
- PEG.Syntax: [Indent] :: forall (n :: Symbol) s (env :: Env) a. Rel n -> PExp s env a -> PExp s env a
+ PEG.Syntax: [Indent] :: forall (n :: Symbol) s (nt :: Type -> Type) a. Rel n -> PExp s nt a -> PExp s nt a
- PEG.Syntax: [Map] :: forall a1 a s (env :: Env). (a1 -> a) -> PExp s env a1 -> PExp s env a
+ PEG.Syntax: [Map] :: forall a1 a s (nt :: Type -> Type). (a1 -> a) -> PExp s nt a1 -> PExp s nt a
- PEG.Syntax: [NT] :: forall (n :: Symbol) s (env :: Env) a. (KnownSymbol n, Lookup n env ~ 'EnvEntry a, KnownMember n env a) => Name n -> PExp s env a
+ PEG.Syntax: [NT] :: forall (nt :: Type -> Type) a s. nt a -> PExp s nt a
- PEG.Syntax: [Not] :: forall s (env :: Env) a1. PExp s env a1 -> PExp s env ()
+ PEG.Syntax: [Not] :: forall s (nt :: Type -> Type) a1. PExp s nt a1 -> PExp s nt ()
- PEG.Syntax: [Position] :: forall (n :: Symbol) s (env :: Env) a. Rel n -> PExp s env a -> PExp s env a
+ PEG.Syntax: [Position] :: forall (n :: Symbol) s (nt :: Type -> Type) a. Rel n -> PExp s nt a -> PExp s nt a
- PEG.Syntax: [Pure] :: forall a s (env :: Env). a -> PExp s env a
+ PEG.Syntax: [Pure] :: forall a s (nt :: Type -> Type). a -> PExp s nt a
- PEG.Syntax: [Sat] :: forall s (env :: Env). !CharSet -> PExp s env Char
+ PEG.Syntax: [Sat] :: forall s (nt :: Type -> Type). !CharSet -> PExp s nt Char
- PEG.Syntax: [Seq] :: forall s (env :: Env) a1 a. PExp s env (a1 -> a) -> PExp s env a1 -> PExp s env a
+ PEG.Syntax: [Seq] :: forall s (nt :: Type -> Type) a1 a. PExp s nt (a1 -> a) -> PExp s nt a1 -> PExp s nt a
- PEG.Syntax: [Span1] :: forall s (env :: Env). !CharSet -> PExp s env s
+ PEG.Syntax: [Span1] :: forall s (nt :: Type -> Type). !CharSet -> PExp s nt s
- PEG.Syntax: [Span] :: forall s (env :: Env). !CharSet -> PExp s env s
+ PEG.Syntax: [Span] :: forall s (nt :: Type -> Type). !CharSet -> PExp s nt s
- PEG.Syntax: [Star] :: forall s (env :: Env) a1. PExp s env a1 -> PExp s env [a1]
+ PEG.Syntax: [Star] :: forall s (nt :: Type -> Type) a1. PExp s nt a1 -> PExp s nt [a1]
- PEG.Syntax: [Str] :: forall s (env :: Env). String -> PExp s env [Char]
+ PEG.Syntax: [Str] :: forall s (nt :: Type -> Type). String -> PExp s nt [Char]
- PEG.Syntax: [Term] :: forall s (env :: Env). Char -> PExp s env Char
+ PEG.Syntax: [Term] :: forall s (nt :: Type -> Type). Char -> PExp s nt Char
- PEG.Syntax: align :: forall s (env :: Env) a. PExp s env a -> PExp s env a
+ PEG.Syntax: align :: forall s (nt :: Type -> Type) a. PExp s nt a -> PExp s nt a
- PEG.Syntax: charClass :: forall s (env :: Env). [(Char, Char)] -> PExp s env Char
+ PEG.Syntax: charClass :: forall s (nt :: Type -> Type). [(Char, Char)] -> PExp s nt Char
- PEG.Syntax: data PExp s (env :: Env) a
+ PEG.Syntax: data PExp s (nt :: Type -> Type) a
- PEG.Syntax: fmapP :: forall a b s (env :: Env). (a -> b) -> PExp s env a -> PExp s env b
+ PEG.Syntax: fmapP :: forall a b s (nt :: Type -> Type). (a -> b) -> PExp s nt a -> PExp s nt b
- PEG.Syntax: indent :: forall (n :: Symbol) s (env :: Env) a. Rel n -> PExp s env a -> PExp s env a
+ PEG.Syntax: indent :: forall (n :: Symbol) s (nt :: Type -> Type) a. Rel n -> PExp s nt a -> PExp s nt a
- PEG.Syntax: notCharClass :: forall s (env :: Env). [(Char, Char)] -> PExp s env Char
+ PEG.Syntax: notCharClass :: forall s (nt :: Type -> Type). [(Char, Char)] -> PExp s nt Char
- PEG.Syntax: nt :: forall (n :: Symbol) (env :: Env) s a. (KnownSymbol n, Lookup n env ~ 'EnvEntry a, KnownMember n env a) => PExp s env a
+ PEG.Syntax: nt :: forall (n :: Symbol) (env :: Env) s a. (Lookup n env ~ 'EnvEntry a, KnownMember n env a) => PExp s (InEnv env) a
- PEG.Syntax: ntw :: forall (n :: Symbol) (env :: Env) s a. (KnownSymbol n, Lookup n env ~ 'EnvEntry a) => Member n env a -> PExp s env a
+ PEG.Syntax: ntw :: forall (n :: Symbol) (env :: Env) s a. Lookup n env ~ 'EnvEntry a => Member n env a -> PExp s (InEnv env) a
- PEG.Syntax: oneOf :: forall s (env :: Env). [Char] -> PExp s env Char
+ PEG.Syntax: oneOf :: forall s (nt :: Type -> Type). [Char] -> PExp s nt Char
- PEG.Syntax: opt :: forall s (env :: Env) a. PExp s env a -> PExp s env (Maybe a)
+ PEG.Syntax: opt :: forall s (nt :: Type -> Type) a. PExp s nt a -> PExp s nt (Maybe a)
- PEG.Syntax: plus :: forall s (env :: Env) a. PExp s env a -> PExp s env [a]
+ PEG.Syntax: plus :: forall s (nt :: Type -> Type) a. PExp s nt a -> PExp s nt [a]
- PEG.Syntax: position :: forall (n :: Symbol) s (env :: Env) a. Rel n -> PExp s env a -> PExp s env a
+ PEG.Syntax: position :: forall (n :: Symbol) s (nt :: Type -> Type) a. Rel n -> PExp s nt a -> PExp s nt a
- PEG.Syntax: pureP :: forall a s (env :: Env). a -> PExp s env a
+ PEG.Syntax: pureP :: forall a s (nt :: Type -> Type). a -> PExp s nt a
- PEG.Syntax: sat :: forall s (env :: Env). CharSet -> PExp s env Char
+ PEG.Syntax: sat :: forall s (nt :: Type -> Type). CharSet -> PExp s nt Char
- PEG.Syntax: spanOf :: forall s (env :: Env). CharSet -> PExp s env s
+ PEG.Syntax: spanOf :: forall s (nt :: Type -> Type). CharSet -> PExp s nt s
- PEG.Syntax: spanOf1 :: forall s (env :: Env). CharSet -> PExp s env s
+ PEG.Syntax: spanOf1 :: forall s (nt :: Type -> Type). CharSet -> PExp s nt s
- PEG.Syntax: stringNE :: forall s (env :: Env). String -> PExp s env String
+ PEG.Syntax: stringNE :: forall s (nt :: Type -> Type). String -> PExp s nt String
Files
- CHANGELOG.md +125/−0
- README.md +70/−20
- bench/Bench/Peg.hs +6/−6
- examples/Arith.hs +9/−7
- examples/Compat.hs +3/−3
- examples/Layout.hs +2/−1
- examples/Main.hs +4/−0
- examples/MiniPython.hs +362/−0
- examples/Patterns.hs +4/−4
- peg-patterns.md +8/−2
- src/PEG.hs +9/−7
- src/PEG/Analysis.hs +122/−41
- src/PEG/Grammar.hs +33/−16
- src/PEG/Key.hs +81/−0
- src/PEG/Member.hs +4/−2
- src/PEG/Parse.hs +79/−37
- src/PEG/QQ.hs +330/−89
- src/PEG/QQ/Compat.hs +27/−0
- src/PEG/QQ/Syntax.hs +3/−3
- src/PEG/Syntax.hs +84/−104
- tests/Analysis.hs +130/−23
- typed-peg.cabal +13/−8
CHANGELOG.md view
@@ -2,6 +2,131 @@ ## Unreleased +### Breaking. A non-terminal is a key, and `pegGrammar` declares the key type++`PExp` and `Grammar` are indexed by the type of the grammar's non-terminal+keys, `nt :: Type -> Type`, instead of by a type-level environment:++```haskell+data PExp (s :: Type) (nt :: Type -> Type) (a :: Type)+data Grammar (s :: Type) (nt :: Type -> Type) (a :: Type)++NT :: nt a -> PExp s nt a+```++`pegGrammar` in declaration position now declares a GADT with one constructor+per rule, its `Tabulate` instance, and one binding per rule whose signature is+the rule's annotation:++```haskell+data ArithEnv s a where+ ArithEnv_expr :: ArithEnv s Exp+ ArithEnv_term :: ArithEnv s Exp+instance Tabulate (ArithEnv s)+arith'expr :: Stream s => PExp s (ArithEnv s) Exp+arith :: Stream s => Grammar s (ArithEnv s) Exp -- = Keyed rules start+```++A grammar written that way needs no change beyond `{-# LANGUAGE GADTs #-}`,+which the splice asks for by name when it is missing: `%env` still names the+generated type and `Grammar s (ArithEnv s) Exp` is still its signature.++A type-level environment is still supported, through the key `InEnv env`, a+membership proof into it. `nt @"expr"`, `ntw`, `pegRules`, `RCons` and+`pegGrammar` in expression position all go through it, and so does a+hand-written environment, whose signatures gain an `InEnv`:++```haskell+calc :: Stream s => Grammar s (InEnv (CalcEnv s)) Expr -- was Grammar s (CalcEnv s) Expr+```++A combinator over expressions, `PExp s env a -> PExp s env a`, works unchanged+over either kind of key. `PEG.Syntax.NTW` is gone (`ntw` remains, as+`NT . InEnv`), and `nt` no longer asks for `KnownSymbol`.++**Why.** Removing the FIRST sets from the environment left a cost that grew+faster than the grammar, and it was the proof itself. A reference into a list+carries `There (There ... Here)`, and GHC's evidence for it is proportional to+the rule's depth times the size of what is left of the list. Handing GHC the+proof instead of having it search saved a constant; the proof still had to be+checked. Through `pegGrammar` a 128-rule grammar needed 2.2 GB of heap, and a+256-rule one did not fit in 8 GB. A constructor's type does not depend on the+rest of the grammar (`ghc -fno-code`, `bench-compile/`):++| rules | before | key type |+|---|---|---|+| 64 | 1.16 s, 385 MiB | 0.44 s, 49 MiB |+| 128 | 5.70 s, 2 180 MiB | 0.50 s, 51 MiB |+| 256 | exhausts 8 GB | 0.57 s, 80 MiB |+| 512 | — | 0.70 s, 98 MiB |+| 1024 | — | 1.17 s, 159 MiB |++The per-rule bindings also cut what the simplifier does with a grammar:+MiniPython at `-O1` went from 2.4 s and 177 MiB to 1.5 s and 111 MiB.++Parsing is unaffected: the allocation benchmark agrees with the previous+commit on every row, to within 0.1 byte per input byte on three of the+smallest inputs, and MiniPython over its key type allocates what it does over+an environment.++### Added++- `PEG.Key`: `Tabulate`, `Table` and `InEnv`.+- `Keyed`, the `Grammar` constructor for a key type.+- `%param name :: Type` in `pegGrammar`: the grammar and every rule take an+ argument in scope in every semantic action, which is what a grammar that+ used to be written in expression position to capture a variable needs.+ Repeatable.+- `examples/MiniPython.hs`, the 27-rule grammar of the MiniPython language of+ the compilers course at UFOP, and a `minipython` mode in `bench-compile/`.+- `bench-compile/run.sh` reports GHC's peak heap, and has `qq-grammar-expr`+ (a grammar over a list, through `pegGrammar`) and `lookup-key` (the keys+ with no library) modes.++### Shared prefixes of alternatives are parsed once++The quasi-quoters translate consecutive alternatives that begin with the same+items as the common prefix followed by a choice of the remainders:+`A B / A C` becomes `A (B / C)`, recursively, with each remainder's action+still seeing the prefix under its own labels. In a PEG the two accept the+same inputs with the same results, since `A` would parse exactly the same+thing the second time.++Without it, a precedence level written `e:or_expr ws "if" ... / e:or_expr`+parses its operand twice, and the cost is exponential in how deeply the+*input* nests. On MiniPython, `print(str(mdc(f(g(x)))))` took 348 ms, eight+times as long per level; a 13.6 KB file of the course's examples took 965 ms.+Both now take under 15 ms. The copy of the library the course's reference+compiler used to vendor escaped this only by accident: its grammar was static+combinator code, and GHC's CSE shared the repeated call. Compiled with+`-O0` it took 6.9 s on the same expression.++Only consecutive alternatives are grouped, and an alternative that is not a+sequence is left alone. `PEG.QQ.Syntax`'s `PExpr`, `Item` and `RelS` now+derive `Eq`.++### Fixed++- The library builds with GHC 9.6 again, as `tested-with` and the+ `template-haskell >= 2.19` bound said it did: `PEG.Parse` no longer needs+ `TypeAbstractions`, the binders `pegGrammar` generates go through+ `PEG.QQ.Compat` rather than naming `BndrReq`, which template-haskell 2.21+ introduced, and the analysis test-suite no longer relies on `foldl'` being+ in the Prelude.++### The analysis is linear++`PEG.Analysis` ran on every splice and computed every rule's FIRST set by+Kleene iteration over whole sets, rebuilding and comparing all of them on+each pass. A precedence ladder of `N` rules has FIRST sets of `N^2/2` names,+and on 1024 rules that was 49 of the 58 seconds a keyed grammar took to+compile. The diagnostics need none of it: nullability is a fixpoint over+booleans, left recursion is a cyclic strongly connected component of the+direct-head graph, and the cycle reported is a shortest one, found breadth+first. The FIRST sets in the environment `analyse` returns are that graph's+closure, built only when the environment is inspected — 0.5 s at 1024 rules+when they are. `typed-peg` now depends on `containers`.+ ### Breaking. The environment no longer carries FIRST sets An entry of a grammar's environment was a rule's nullability, its FIRST set
README.md view
@@ -2,9 +2,11 @@ Type-safe PEG (Parsing Expression Grammar) parser combinators for Haskell. -Grammar non-terminals are checked at the type level against an environment-that binds each rule to the type it returns, and left-recursive grammars are-caught when the grammar is written rather than looping at runtime.+Grammar non-terminals are checked at the type level — a reference is a key+whose type is the result of the rule it names — and left-recursive grammars+are caught when the grammar is written rather than looping at runtime.+Checking a grammar costs GHC time and memory linear in its size: a 1024-rule+grammar type-checks in about a second. ## Features @@ -43,7 +45,7 @@ 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 `forall s. Stream s => Grammar s (Env s) 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: @@ -72,28 +74,72 @@ |] ``` -That declares three things: `type ArithEnv s`, the signature-`arith :: Stream s => Grammar s (ArithEnv s) Exp`, and `arith` itself. Run-it with `parse arith "1+2*3"`.+That declares the grammar's key type, one constructor per rule, +```haskell+data ArithEnv s a where+ ArithEnv_expr :: ArithEnv s Exp+ ArithEnv_term :: ArithEnv s Exp+ ...+```++a binding per rule (`arith'expr :: Stream s => PExp s (ArithEnv s) Exp`, ...),+and `arith :: Stream s => Grammar s (ArithEnv s) Exp`. Run it with+`parse arith "1+2*3"`. The module needs `GADTs`.+ A rule's **result type** is the one thing the grammar does not determine — it comes from the Haskell in the semantic action — which is what the `:: T`-annotations are for. They are claims, and GHC checks them: `Grammar` demands-`Rules s env env`, so an annotation that disagrees with what the body returns-is a type error.+annotations are for. They are claims, and GHC checks them: each is the+signature of that rule's binding, so an annotation that disagrees with what the+body returns is a type error reported against the rule. +A grammar that needs a value from outside — a file name for error positions,+a table of operators — takes it as a parameter:++```haskell+[pegGrammar|+ %name lang+ %stream String+ %param file :: FilePath+ %start program+ ...+|]+-- lang :: FilePath -> Grammar String (LangEnv String) Program+```+ `pegRules` remains, for a rule set that is only part of a grammar or that is-combined with hand-written `PExp` combinators. It needs the environment-written out by hand; `examples/Compat.hs` and `examples/Patterns.hs` show-that style. See `examples/Arith.hs` and `examples/Layout.hs` for the-generated one.+combined with hand-written `PExp` combinators. Its non-terminals are a+type-level list written out by hand, and the grammar is a+`Grammar s (InEnv env) a`; `examples/Compat.hs` and `examples/Patterns.hs`+show that style. See `examples/Arith.hs`, `examples/Layout.hs` and+`examples/MiniPython.hs` for the generated one. ## Grammar size, and what is checked where A grammar's size shows up as compile time, because every reference in it is a-constraint GHC has to solve against the environment. An entry of that-environment is a rule's name and the type it returns:+constraint GHC has to solve. How much each one costs depends on what the+reference points into. +**A declared key type** — what `pegGrammar` generates — makes a reference a+constructor, `NT ArithEnv_term`, whose type GHC checks without looking at the+rest of the grammar. The cost is linear in the number of rules+(`ghc -fno-code`, GHC 9.10; `bench-compile/`):++| rules | through a key type | through a type-level list |+|---|---|---|+| 64 | 0.44 s, 49 MiB | 1.10 s, 375 MiB |+| 128 | 0.50 s, 51 MiB | 5.74 s, 2 298 MiB |+| 256 | 0.57 s, 80 MiB | exhausts 8 GB |+| 1024 | 1.17 s, 159 MiB | — |++The 27-rule MiniPython grammar in `examples/` takes 0.43 s and 57 MiB.++**A type-level list** is what `nt @"expr"`, `pegRules` and `pegGrammar` in+expression position use. A reference carries a proof of where its rule sits+in the list, `There (There Here)`, and GHC's evidence for it grows with the+rule's depth and the size of the list, so the total grows faster than the+grammar. An entry of that list is a rule's name and the type it returns:+ ```haskell type CalcEnv = '[ '("expr" , 'EnvEntry Expr)@@ -114,7 +160,7 @@ the measurements. Nullability and FIRST sets are still computed — by `PEG.Analysis`, in ordinary-Haskell, when the quasi-quoter runs, in 6 ms for a 64-rule grammar. It is+Haskell, when the quasi-quoter runs, in time linear in the grammar. It is what reports left recursion, a nullable repetition, an undefined non-terminal or a duplicate rule **from the splice**, naming the rule and the chain of head references that closes the cycle:@@ -128,11 +174,16 @@ would not consume input before calling itself ``` +A PEG does not memoise, so `A B / A C` parses `A` twice when `B` fails. The+quasi-quoters factor such alternatives into `A (B / C)` when they are+consecutive, which keeps a grammar whose precedence levels are written that+way linear in the input rather than exponential in its nesting.+ So the checks divide like this: | what | checked by | when | |---|---|---|-| a reference names a rule that exists, at the right type | GHC | every compilation |+| a reference names a rule that exists, at the right type | GHC, or the splice for `pegGrammar` | every compilation | | a rule's `:: T` annotation matches what its body returns | GHC | every compilation | | left recursion, nullable repetition, duplicate rule | `PEG.Analysis` | at the splice | @@ -143,8 +194,7 @@ closes *across* two blocks is reported by neither it nor GHC. Writing the grammar as one `pegGrammar` closes both gaps — it is closed-world, so every reference resolves and every cycle is visible — and it is also the fastest to-compile, because it knows each rule's position and emits the membership proof-instead of a `KnownMember` search.+compile, because it declares a key type instead of a list. Since nothing recomputes what `PEG.Analysis` concludes, the `typed-peg-analysis` test-suite checks it against a separate statement of what
bench/Bench/Peg.hs view
@@ -69,7 +69,7 @@ ] {-# INLINABLE arith #-}-arith :: Stream s => Grammar s ArithEnv Exp+arith :: Stream s => Grammar s (InEnv ArithEnv) Exp arith = Grammar [pegRules|@@ -93,7 +93,7 @@ ] {-# INLINABLE csv #-}-csv :: Stream s => Grammar s CsvEnv [[Int]]+csv :: Stream s => Grammar s (InEnv CsvEnv) [[Int]] csv = Grammar [pegRules|@@ -118,7 +118,7 @@ ] {-# INLINABLE idents #-}-idents :: Stream s => Grammar s (IdentEnv s) [s]+idents :: Stream s => Grammar s (InEnv (IdentEnv s)) [s] idents = Grammar [pegRules|@@ -162,7 +162,7 @@ ] {-# INLINABLE json #-}-json :: Stream s => Grammar s JsonEnv JValue+json :: Stream s => Grammar s (InEnv JsonEnv) JValue json = Grammar [pegRules|@@ -207,7 +207,7 @@ ] {-# INLINABLE quotedNot #-}-quotedNot :: Stream s => Grammar s QuotedNotEnv [String]+quotedNot :: Stream s => Grammar s (InEnv QuotedNotEnv) [String] quotedNot = Grammar [pegRules|@@ -217,7 +217,7 @@ (nt @"qs") {-# INLINABLE quotedCls #-}-quotedCls :: Stream s => Grammar s (QuotedClsEnv s) [s]+quotedCls :: Stream s => Grammar s (InEnv (QuotedClsEnv s)) [s] quotedCls = Grammar [pegRules|
examples/Arith.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE DataKinds #-}+{-# LANGUAGE GADTs #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-}@@ -50,14 +51,15 @@ addOp l ('/', r) = Div l r addOp _ (c , _) = error ("addOp: unexpected operator " ++ show c) --- | The environment, the signature and the grammar are all declared by the--- quasi-quoter. A rule's result type is the one thing the grammar does not--- determine, which is what the @:: T@ annotations are for; left recursion and--- the rest are checked by 'PEG.Analysis' at the splice.+-- | The key type, a binding per rule, the signature and the grammar are all+-- declared by the quasi-quoter. A rule's result type is the one thing the+-- grammar does not determine, which is what the @:: T@ annotations are for;+-- left recursion and the rest are checked by 'PEG.Analysis' at the splice. ----- The annotations are still claims that GHC checks, not assertions:--- 'PEG.Grammar.Grammar' demands @Rules s env env@, so an annotation that--- disagrees with what the rule body actually returns is a type error here.+-- The annotations are still claims that GHC checks, not assertions: each is+-- the signature of its rule's binding (@arith'term :: Stream s => PExp s+-- (ArithEnv s) Exp@), so an annotation that disagrees with what the rule body+-- actually returns is a type error here, reported against that rule. -- -- Being polymorphic in the stream has a cost: this is a function of a -- 'Stream' dictionary rather than a constant, so the compiled parser is not
examples/Compat.hs view
@@ -83,7 +83,7 @@ , '("digits1", 'EnvEntry s) ] -spanG :: Stream s => Grammar s (SpanEnv s) (s, s)+spanG :: Stream s => Grammar s (InEnv (SpanEnv s)) (s, s) spanG = Grammar [pegRules|@@ -94,10 +94,10 @@ -- @!'x'+ .@ accepts any character that is not an @x@; @!'x'* .@ accepts -- nothing at all.-notSpan1G :: Stream s => Grammar s '[] Char+notSpan1G :: Stream s => Grammar s (InEnv '[]) Char notSpan1G = Grammar RNil [pegExpr| !'x'+ c:. |] -notSpanG :: Stream s => Grammar s '[] Char+notSpanG :: Stream s => Grammar s (InEnv '[]) Char notSpanG = Grammar RNil [pegExpr| !'x'* c:. |] spanCases :: [String]
examples/Layout.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE DataKinds #-}+{-# LANGUAGE GADTs #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-}@@ -21,7 +22,7 @@ -- | @name@ is a character class, so its result is a chunk of the input rather -- than a 'String' — which is why its annotation is @s@ and why the generated--- environment takes the stream as a parameter.+-- key type takes the stream as a parameter. -- -- @ws@ has neither a label nor an action, so it returns @()@: that is the -- DSL's rule for a rule body, and the annotation has to agree with it. A
examples/Main.hs view
@@ -5,6 +5,7 @@ import Layout (doExp, layoutOpts) import Compat (compatMain) import Patterns (patternsMain)+import MiniPython (miniPythonMain) showResult :: Show a => Result String a -> String showResult (OK a _ _) = "OK " ++ show a@@ -28,6 +29,9 @@ putStrLn "\n=== Patterns (see peg-patterns.md) ===" patternsMain++ putStrLn "\n=== MiniPython ==="+ miniPythonMain putStrLn "\n=== Differential battery ===" compatMain
+ examples/MiniPython.hs view
@@ -0,0 +1,362 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-}++-- | The grammar of MiniPython, the language of the compilers course at UFOP.+--+-- It is ported from the course's reference implementation (the @mpyc@+-- compiler, @MiniPython.ParserPEG@), with one change to the AST: source+-- positions are dropped, since that grammar filled every one of them with+-- @Pos file 0 0@ anyway.+--+-- Two things the specification has and that grammar lacked are added, as+-- they were to the course's parsers: a method's explicit @self@ parameter,+-- and a field declared in the body of a class with a type and no initialiser+-- (@x: int@), which is 'SFieldDecl'.+--+-- It is here for its size rather than for its language. With 27 rules, a+-- dozen precedence levels and some very long ordered choices it is the+-- largest grammar in @examples/@, and a realistic check on what a generated+-- grammar costs the type checker. While environments carried FIRST sets (see+-- "PEG.Type"), this grammar with an inferred environment exhausted a 10 GB+-- heap after four minutes. Over a generated key type (see "PEG.Key") it+-- type-checks in under half a second and 57 MiB; @bench-compile/@ measures it+-- as the @minipython@ mode.+--+-- Blocks are indentation-sensitive: @block@ requires its statements to be+-- indented further than the line that opened it, and each statement to be+-- aligned with the first one. Run it with 'mpyOpts'.+module MiniPython+ ( AnnType (..)+ , BinOp (..)+ , UnOp (..)+ , Expr (..)+ , Param (..)+ , Stmt (..)+ , Program (..)+ , MiniPythonEnv+ , miniPython+ , mpyOpts+ , parseMiniPython+ , miniPythonMain+ ) where++import PEG hiding (Not)+import PEG.QQ (pegGrammar)++type Ident = String++data AnnType+ = ATInt+ | ATFloat+ | ATBool+ | ATStr+ | ATNone+ | ATList AnnType+ | ATFunc [AnnType] AnnType+ | ATClass Ident+ deriving (Show, Eq)++data BinOp+ = Add | Sub | Mul | Div | IDiv | Mod | Pow+ | Eq | Ne | Lt | Le | Gt | Ge+ | And | Or+ deriving (Show, Eq)++data UnOp = Neg | Not+ deriving (Show, Eq)++data Expr+ = EInt Integer+ | EFloat Double+ | EBool Bool+ | EStr String+ | ENone+ | EVar Ident+ | EBinOp BinOp Expr Expr+ | EUnOp UnOp Expr+ | ECall Expr [Expr]+ | EIndex Expr Expr+ | EField Expr Ident+ | EList [Expr]+ | ELambda [Ident] Expr+ | EIfExpr Expr Expr Expr+ deriving (Show, Eq)++data Param = Param Ident (Maybe AnnType)+ deriving (Show, Eq)++data Stmt+ = SAssign Ident (Maybe AnnType) Expr+ | SAssignIdx Expr Expr Expr+ | SAssignFld Expr Ident Expr+ | SAugAssign Ident BinOp Expr+ | SExpr Expr+ | SIf [(Expr, [Stmt])] (Maybe [Stmt])+ | SWhile Expr [Stmt]+ | SFor Ident Expr [Stmt]+ | SReturn (Maybe Expr)+ | SBreak+ | SContinue+ | SPass+ | SDef Ident [Param] (Maybe AnnType) [Stmt]+ | SClass Ident (Maybe Ident) [Stmt]+ | SFieldDecl Ident AnnType+ deriving (Show, Eq)++newtype Program = Program [Stmt]+ deriving (Show, Eq)++data Postfix+ = PFCall [Expr]+ | PFIndex Expr+ | PFField Ident++applyPostfixes :: Expr -> [Postfix] -> Expr+applyPostfixes = foldl step+ where+ step e (PFCall args) = ECall e args+ step e (PFIndex idx) = EIndex e idx+ step e (PFField n) = EField e n++-- | The left-hand side of an assignment is parsed as an expression and+-- checked afterwards, as Python's own grammar does.+assignTo :: Expr -> Expr -> Stmt+assignTo (EField base n) rhs = SAssignFld base n rhs+assignTo (EIndex base i) rhs = SAssignIdx base i rhs+assignTo (EVar n) rhs = SAssign n Nothing rhs+assignTo e rhs = SExpr (EBinOp Eq e rhs)++binLeft :: BinOp -> Expr -> [Expr] -> Expr+binLeft o = foldl (EBinOp o)++binOps :: Expr -> [(BinOp, Expr)] -> Expr+binOps = foldl (\l (o, r) -> EBinOp o l r)++-- | A keyword is a literal not followed by an identifier character; every+-- @![a-zA-Z0-9_]@ below is that lookahead.+[pegGrammar|+ %name miniPython+ %start program+ %stream String++ program :: Program <- ws ss:(ws st:|s:stmtbody|)* ws !. { Program ss }++ ws :: () <- ([ \t\n\r] / '#' [^\n]* ('\n' / !.))*_~++ name :: String <-+ !( ( "and" / "break" / "bool" / "class" / "continue"+ / "def" / "elif" / "else" / "False" / "float"+ / "for" / "if" / "int" / "in" / "lambda"+ / "None" / "not" / "or" / "pass" / "return"+ / "self" / "str" / "True" / "while" ) ![a-zA-Z0-9_] )+ c:[a-zA-Z_] cs:[a-zA-Z0-9_]* { c : cs }++ block :: [Stmt] <- ':' ss:(ws st:|s:stmtbody|)+^> { ss }++ stmtbody :: Stmt <-+ d:def_stmt { d }+ / cl:class_stmt { cl }+ / i:if_stmt { i }+ / "while" ![a-zA-Z0-9_] ws wc:expr ws wb:block { SWhile wc wb }+ / "for" ![a-zA-Z0-9_] ws fn:name ws "in" ![a-zA-Z0-9_] ws fe:expr ws fb:block+ { SFor fn fe fb }+ / "return" ![a-zA-Z0-9_] ws re:(re2:expr { Just re2 } / { Nothing })+ { SReturn re }+ / "break" ![a-zA-Z0-9_] { SBreak }+ / "continue" ![a-zA-Z0-9_] { SContinue }+ / "pass" ![a-zA-Z0-9_] { SPass }+ / a:assign_stmt { a }++ def_stmt :: Stmt <-+ "def" ![a-zA-Z0-9_] ws n:name ws '(' ws+ ps:( p:param rest:(ws ',' ws q:param { q })* { p : rest }+ / { [] } )+ ws ')'+ rt:(ws "->" ws t:ann_type { Just t } / { Nothing })+ ws b:block+ { SDef n ps rt b }++ param :: Param <-+ "self" ![a-zA-Z0-9_] { Param "self" Nothing }+ / pn:name ws ':' ws pt:ann_type { Param pn (Just pt) }+ / pn:name { Param pn Nothing }++ class_stmt :: Stmt <-+ "class" ![a-zA-Z0-9_] ws n:name ws+ par:('(' ws cn:name ws ')' { Just cn } / { Nothing })+ ws b:class_block+ { SClass n par b }++ class_block :: [Stmt] <- ':' ss:(ws st:|s:class_member|)+^> { ss }++ class_member :: Stmt <-+ n:name ws ':' ws t:ann_type !(ws '=' !'=') { SFieldDecl n t }+ / s:stmtbody { s }++ if_stmt :: Stmt <-+ "if" ![a-zA-Z0-9_] ws c:expr ws b:block+ elifs:(ws "elif" ![a-zA-Z0-9_] ws ec:expr ws eb:block { (ec, eb) })*+ alt:(ws "else" ![a-zA-Z0-9_] ws ab:block { Just ab } / { Nothing })+ { SIf ((c, b) : elifs) alt }++ assign_stmt :: Stmt <-+ n:name ws ':' ws t:ann_type ws '=' !'=' ws e:expr { SAssign n (Just t) e }+ / n:name ws "//=" ws e:expr { SAugAssign n IDiv e }+ / n:name ws "+=" ws e:expr { SAugAssign n Add e }+ / n:name ws "-=" ws e:expr { SAugAssign n Sub e }+ / n:name ws "*=" ws e:expr { SAugAssign n Mul e }+ / n:name ws "/=" ws e:expr { SAugAssign n Div e }+ / n:name ws "%=" ws e:expr { SAugAssign n Mod e }+ / e:postfix_expr ws '=' !'=' ws r:expr { assignTo e r }+ / e:expr { SExpr e }++ ann_type :: AnnType <-+ "int" ![a-zA-Z0-9_] { ATInt }+ / "float" ![a-zA-Z0-9_] { ATFloat }+ / "bool" ![a-zA-Z0-9_] { ATBool }+ / "str" ![a-zA-Z0-9_] { ATStr }+ / "None" ![a-zA-Z0-9_] { ATNone }+ / '[' ws t:ann_type ws ']' { ATList t }+ / '(' ws ts:(t:ann_type ts2:(ws ',' ws u:ann_type { u })* { t : ts2 } / { [] })+ ws ')' ws "->" ws r:ann_type+ { ATFunc ts r }+ / n:name ws '[' ws t2:ann_type ws ']'+ { if n == "list" then ATList t2 else ATClass n }+ / n2:name { ATClass n2 }++ expr :: Expr <-+ "lambda" ![a-zA-Z0-9_] ws+ ps:(n:name ns:(ws ',' ws m:name { m })* { n : ns } / { [] })+ ws ':' ws b:expr+ { ELambda ps b }+ / e:or_expr ws "if" ![a-zA-Z0-9_] ws c:or_expr ws "else" ![a-zA-Z0-9_] ws a:expr+ { EIfExpr c e a }+ / e:or_expr { e }++ or_expr :: Expr <- e:and_expr es:(ws "or" ![a-zA-Z0-9_] ws r:and_expr)*+ { binLeft Or e es }++ and_expr :: Expr <- e:not_expr es:(ws "and" ![a-zA-Z0-9_] ws r:not_expr)*+ { binLeft And e es }++ not_expr :: Expr <-+ "not" ![a-zA-Z0-9_] ws e:not_expr { EUnOp Not e }+ / e:add_expr ws o:cmp_op ws r:add_expr { EBinOp o e r }+ / e:add_expr { e }++ cmp_op :: BinOp <-+ "==" { Eq } / "!=" { Ne } / "<=" { Le } / ">=" { Ge } / '<' { Lt } / '>' { Gt }++ add_expr :: Expr <-+ e:mul_expr es:(ws o:('+' { Add } / '-' { Sub }) ws r:mul_expr { (o, r) })*+ { binOps e es }++ mul_expr :: Expr <-+ e:pow_expr+ es:(ws o:("//" { IDiv } / '/' { Div } / '%' { Mod } / '*' !'*' { Mul })+ ws r:pow_expr { (o, r) })*+ { binOps e es }++ pow_expr :: Expr <- e:unary_expr ws "**" ws r:pow_expr { EBinOp Pow e r }+ / e:unary_expr { e }++ unary_expr :: Expr <- '-' ws e:unary_expr { EUnOp Neg e }+ / e:postfix_expr { e }++ postfix_expr :: Expr <-+ e:atom+ ps:( ws '(' ws as:(ea:expr eas:(ws ',' ws r:expr { r })* { ea : eas } / { [] })+ ws ')' { PFCall as }+ / ws '[' ws i:expr ws ']' { PFIndex i }+ / ws '.' ws fn:name { PFField fn } )*+ { applyPostfixes e ps }++ atom :: Expr <-+ "True" ![a-zA-Z0-9_] { EBool True }+ / "False" ![a-zA-Z0-9_] { EBool False }+ / "None" ![a-zA-Z0-9_] { ENone }+ / "self" ![a-zA-Z0-9_] { EVar "self" }+ / "int" ![a-zA-Z0-9_] { EVar "int" }+ / "float" ![a-zA-Z0-9_] { EVar "float" }+ / "bool" ![a-zA-Z0-9_] { EVar "bool" }+ / "str" ![a-zA-Z0-9_] { EVar "str" }+ / n:name { EVar n }+ / ds:[0-9]+ '.' fs:[0-9]+ ep:exponent+ { EFloat (read (ds ++ "." ++ fs ++ ep) :: Double) }+ / ds:[0-9]+ ep:(e:[eE] s:sign xs:[0-9]+ { (e : s) ++ xs })+ { EFloat (read (ds ++ ep) :: Double) }+ / d:[0-9] ds:[0-9_]* { EInt (read (d : filter (/= '_') ds) :: Integer) }+ / '"' cs:(c:str_char { c } / !'"' c3:. { c3 })* '"' { EStr cs }+ / '\'' cs:(c:str_char { c } / !'\'' c3:. { c3 })* '\'' { EStr cs }+ / '(' ws e:expr ws ')' { e }+ / '[' ws les:(le:expr les2:(ws ',' ws lr:expr { lr })* { le : les2 } / { [] })+ ws ']'+ { EList les }++ exponent :: String <-+ e:[eE] s:sign xs:[0-9]+ { (e : s) ++ xs }+ / { "" }++ sign :: String <- '+' { "+" } / '-' { "-" } / { "" }++ str_char :: Char <-+ '\\' c:( 'n' { '\n' } / 't' { '\t' } / 'r' { '\r' }+ / '\\' { '\\' } / '"' { '"' } / '\'' { '\'' }+ / '0' { '\0' } / c2:. { c2 } )+ { c }+|]++-- | Blocks are delimited by indentation, so tokens are compared with the+-- enclosing block's column.+mpyOpts :: Opts+mpyOpts = defaultOpts { optTokenMode = relD geR }++parseMiniPython :: String -> Maybe Program+parseMiniPython src = case parseWith mpyOpts miniPython src of+ OK p _ _ -> Just p+ Fail -> Nothing++miniPythonMain :: IO ()+miniPythonMain = mapM_ run samples+ where+ run (label, src) = putStrLn $ label ++ " => " ++ case parseMiniPython src of+ Just (Program ss) -> "OK, " ++ show (length ss) ++ " statement(s): "+ ++ take 100 (show ss)+ Nothing -> "Fail"++ samples =+ [ ("assign", "x: int = 1 + 2 * 3\n")+ , ("def", unlines+ [ "def fib(n: int) -> int:"+ , " if n < 2:"+ , " return n"+ , " else:"+ , " return fib(n - 1) + fib(n - 2)"+ , "print(fib(10))"+ ])+ , ("while", unlines+ [ "i = 0"+ , "while i < 10:"+ , " i += 1"+ , " if i % 2 == 0:"+ , " continue"+ , " xs[i] = i ** 2"+ ])+ , ("class", unlines+ [ "class Point:"+ , " x: float"+ , " y: float"+ , " def __init__(self, x: float, y: float) -> None:"+ , " self.x = x"+ , " self.y = y"+ , " def norm(self) -> float:"+ , " return self.x * self.x + self.y * self.y"+ , "p = Point(3.0, 4.0)"+ , "s = 'a\\n' if not p.norm() >= 1.5e3 else \"b\""+ ])+ , ("missing colon", "if x > 1\n pass\n")+ ]
examples/Patterns.hs view
@@ -139,7 +139,7 @@ -- Note what is /not/ here: no @try@, no left recursion, and no rule that can -- loop. @expr <- expr '+' term@ would be rejected by 'PEG.Analysis' when the -- @pegRules@ block below is spliced, naming @expr@ and the cycle.-calc :: Stream s => Grammar s (CalcEnv s) Expr+calc :: Stream s => Grammar s (InEnv (CalcEnv s)) Expr calc = Grammar [pegRules|@@ -157,7 +157,7 @@ -- by a negative lookahead on the identifier-continuation class. type KwEnv = '[ '("kw", 'EnvEntry String) ] -kwG :: Stream s => Grammar s KwEnv String+kwG :: Stream s => Grammar s (InEnv KwEnv) String kwG = Grammar [pegRules| kw <- k:"negate" ![a-zA-Z0-9_] { k } |] (nt @"kw") --------------------------------------------------------------------------------@@ -172,7 +172,7 @@ type OpEnv = '[ '("op", 'EnvEntry (Expr -> Expr -> Expr)) ] -addOp :: Stream s => Grammar s OpEnv (Expr -> Expr -> Expr)+addOp :: Stream s => Grammar s (InEnv OpEnv) (Expr -> Expr -> Expr) addOp = Grammar [pegRules| op <- '+' { Add } / '-' { Sub } |] (nt @"op") --------------------------------------------------------------------------------@@ -194,7 +194,7 @@ -- 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 :: Stream s => Grammar s (InEnv (ProgEnv s)) [Asgn] prog = Grammar [pegRules|
peg-patterns.md view
@@ -154,12 +154,18 @@ to write down is the part only you know: what the rule returns. Better still, do not write it down at all. `pegGrammar` generates the-environment from the same `:: T` annotations:+grammar's non-terminals from the same `:: T` annotations — as a key type with+one constructor per rule, `data CalcEnv s a where CalcEnv_expr :: CalcEnv s+Expr; ...`, rather than as a list — and that is also what keeps a grammar of+hundreds of rules cheap to compile: ``` expr :: Expr <- t:term ts:(o:[+-] u:term)* { chainl t ts } ``` +A grammar over a hand-written list names it through `InEnv`:+`Grammar s (InEnv (CalcEnv s)) Expr`.+ ### 1.4 Precedence tables: absent, but not impossible *(Willis & Wu, Pattern 1c: Precedence Tables.)*@@ -372,7 +378,7 @@ ```haskell type OpEnv = '[ '("op", 'EnvEntry (Expr -> Expr -> Expr)) ] -addOp :: Stream s => Grammar s OpEnv (Expr -> Expr -> Expr)+addOp :: Stream s => Grammar s (InEnv OpEnv) (Expr -> Expr -> Expr) addOp = Grammar [pegRules| op <- '+' { Add } / '-' { Sub } |] (nt @"op") ```
src/PEG.hs view
@@ -16,15 +16,15 @@ -- @ -- -- 1. Write the grammar with the 'PEG.QQ.pegGrammar' quasi-quoter, giving each--- rule its result type. It declares the environment, the grammar and its--- signature.+-- rule its result type. It declares the grammar's key type — one+-- constructor per rule, see "PEG.Key" — the grammar and its signature. -- 2. Run the grammar on a 'String' with 'parse' or 'parseWith'. ----- The environment can also be declared by hand — a type-level list of--- @(name, entry)@ pairs, see 'PEG.Type.Env' — and the rules built with--- 'PEG.QQ.pegRules' or the combinators in "PEG.Syntax". That is what--- 'PEG.QQ.pegGrammar' generates, and it stays supported; it is only more to--- write and slower to compile.+-- The rules can also be named by a type-level list declared by hand — see+-- 'PEG.Type.Env' — and built with 'PEG.QQ.pegRules' or the combinators in+-- "PEG.Syntax", as a @'Grammar' s ('InEnv' env) a@. That stays supported; it+-- is more to write, and compiling it costs more than linearly in the number of+-- rules. -- -- See the @examples/@ directory for complete working grammars. module PEG@@ -33,6 +33,7 @@ , module PEG.Type , module PEG.TyLevel , module PEG.Member+ , module PEG.Key , module PEG.Indent , module PEG.Syntax , module PEG.Grammar@@ -43,6 +44,7 @@ import PEG.Grammar import PEG.Stream import PEG.Indent+import PEG.Key import PEG.Member import PEG.Parse import PEG.Syntax
src/PEG/Analysis.hs view
@@ -56,8 +56,13 @@ , spannable ) where -import Data.List (foldl1', nub)-import Data.Maybe (fromMaybe)+import Data.Graph (SCC (..), stronglyConnComp)+import Data.List (foldl1')+import qualified Data.List as L+import qualified Data.Map.Strict as M+import Data.Sequence (Seq (..), (|>))+import qualified Data.Sequence as Seq+import qualified Data.Set as S import PEG.QQ.Syntax (Def (..), Item (..), PExpr (..)) @@ -217,6 +222,27 @@ analyse = analyseWith Closed -- | 'analyse', over a whole grammar or a fragment of one.+--+-- == Cost+--+-- This runs inside every splice of a grammar, so its cost is paid on every+-- compilation of the module, and it is written to be linear in the grammar+-- wherever the answer allows. Nothing the diagnostics need is a FIRST set:+--+-- * Nullability is a least fixpoint over booleans alone.+-- * The /direct/ head references of each rule — 'exprTy' again, with every+-- rule's FIRST set taken to be empty — form a graph, and a rule is+-- left-recursive exactly when it lies on a cycle of that graph, which its+-- strongly connected components say directly.+-- * The cycle reported is a shortest one, found by a breadth-first search+-- inside the rule's component.+--+-- The FIRST sets in the environment returned are the transitive closure of+-- that graph, and are computed only if the environment is inspected. They+-- used to be computed, by Kleene iteration over whole sets, on every splice:+-- a precedence ladder of @N@ rules has FIRST sets of @N^2/2@ names in total,+-- and each iteration rebuilt and compared all of them, which on 1024 rules+-- was 49 seconds of a 58-second compile. analyseWith :: World -> [Def] -> Either [Diagnostic] RuleEnv analyseWith world defs | not (null dups) = Left dups@@ -225,73 +251,128 @@ | not (null leftRecs) = Left leftRecs | otherwise = Right env where- names = [ n | Def n _ _ <- defs ]+ names = [ n | Def n _ _ <- defs ]+ defined = S.fromList names - dups = [ DuplicateRule n- | n <- nub names, length (filter (== n) names) > 1 ]+ -- The first definition of each name, as 'bodyOf' used to take it.+ bodies :: M.Map String PExpr+ bodies = M.fromList [ (n, e) | Def n _ e <- reverse defs ] + dups = [ DuplicateRule n | n <- nubOrd names, M.findWithDefault 0 n counts > 1 ]+ where counts = M.fromListWith (+) [ (n, 1 :: Int) | n <- names ]+ undefs = case world of Open -> [] Closed -> [ UndefinedNT n names- | n <- nub (concatMap (refs . body) defs), n `notElem` names ]- where body (Def _ _ e) = e+ | n <- nubOrd (concatMap (\(Def _ _ e) -> refs e) defs)+ , not (S.member n defined) ] - -- Kleene iteration from the empty environment. Every clause of 'exprTy'- -- is monotone in the environment and the lattice is finite, so this- -- terminates; it is the least solution of the equations the type families- -- state.- env = fix [ (n, Ty False []) | n <- names ]+ -- Kleene iteration over nullability alone, from "nothing is nullable".+ -- Each pass updates the rules in order, reading what earlier rules of+ -- the same pass concluded, and stops at the first pass that changes+ -- nothing; that is still the least solution, since every step is+ -- monotone.+ nullables :: M.Map String Bool+ nullables = fix (M.fromList [ (n, False) | n <- names ]) where- fix m = let m' = step m in if m' == m then m else fix m'- step m = [ (n, exprTy (at m) e) | Def n _ e <- defs ]+ fix m = case L.foldl' update (m, False) defs of+ (m', True) -> fix m'+ (m', _) -> m'+ update (m, changed) (Def n _ e) =+ let new = tyNullable (exprTy (headsOnly m) e)+ in if new && not (M.findWithDefault False n m)+ then (M.insert n True m, True)+ else (m, changed) - at m n = fromMaybe (Ty False []) (lookup n m)+ -- An environment that knows each rule's nullability and nothing of its+ -- FIRST set, under which 'exprTy' yields an expression's /direct/ heads.+ headsOnly m k = Ty (M.findWithDefault False k m) [] + nullableEnv = headsOnly nullables++ -- The graph of direct head references. A name no rule defines is a node+ -- with no successors, as it is opaque in an 'Open' block.+ heads :: M.Map String [String]+ heads = M.map (tyFirst . exprTy nullableEnv) bodies++ succs k = M.findWithDefault [] k heads+ -- A repetition must consume input, which 'PEG.Syntax.Star' states as a -- non-nullable operand. Checking it here names the rule it is in.- illFormed = [ NullableStar n | Def n _ e <- defs, hasNullableRep (at env) e ]+ illFormed = [ NullableStar n | Def n _ e <- defs, hasNullableRep nullableEnv e ] + -- Each rule's strongly connected component. A rule is on a cycle when+ -- its component is cyclic, which includes a rule heading itself.+ component :: M.Map String Int+ component = M.fromList+ [ (n, i) | (i, CyclicSCC ns) <- zip [0 ..] sccs, n <- ns ]+ where+ sccs = stronglyConnComp+ [ (n, n, [ h | h <- succs n, S.member h defined ])+ | n <- M.keys bodies ]+ -- Every rule on a cycle is left-recursive, and reporting each of them -- prints the same cycle once per entry point. Two paths that are -- rotations of each other are the same cycle, so only the first is kept.- leftRecs = dedupe [] [ LeftRecursive n (cycleFrom n)- | (n, t) <- env, n `elem` tyFirst t ]+ leftRecs = dedupe S.empty [ LeftRecursive n (cycleFrom n)+ | n <- names, M.member n component ] where dedupe _ [] = [] dedupe seen (d@(LeftRecursive _ path) : rest)- | key `elem` seen = dedupe seen rest- | otherwise = d : dedupe (key : seen) rest+ | S.member key seen = dedupe seen rest+ | otherwise = d : dedupe (S.insert key seen) rest where key = canonical path dedupe seen (d : rest) = d : dedupe seen rest -- A cycle is written as @n -> ... -> n@; drop the repeated end and turn- -- it so that it starts at its least name.+ -- it so that it starts at its least name. A shortest cycle visits no+ -- rule twice, so that rotation is unique. canonical path = case reverse (drop 1 (reverse path)) of [] -> []- nodes -> minimum [ rotate k nodes | k <- [0 .. length nodes - 1] ]- where rotate k xs = drop k xs ++ take k xs+ nodes -> let (pre, post) = break (== minimum nodes) nodes+ in post ++ pre - -- The FIRST set is already transitive, so it says /that/ a rule is- -- left-recursive but not /how/. The chain is recovered from the graph of- -- direct head references, which is 'exprTy' again with the environment- -- cut back to nullability alone.- heads n = tyFirst (exprTy (\k -> Ty (tyNullable (at env k)) []) (bodyOf n))+ -- A shortest cycle through @n@: breadth first from @n@, never leaving its+ -- component, until a rule that heads @n@ is dequeued.+ cycleFrom n = go (M.singleton n n) (Seq.singleton n)+ where+ comp = M.lookup n component+ go _ Empty = []+ go parents (cur :<| queue)+ | n `elem` succs cur = reverse (pathTo parents cur) ++ [n]+ | otherwise =+ let new = [ h | h <- nubOrd (succs cur)+ , M.lookup h component == comp+ , not (M.member h parents) ]+ parents' = L.foldl' (\m h -> M.insert h cur m) parents new+ in go parents' (L.foldl' (|>) queue new)+ pathTo parents cur+ | cur == n = [n]+ | otherwise = cur : pathTo parents (M.findWithDefault n cur parents) - bodyOf n = case [ e | Def m _ e <- defs, m == n ] of- (e:_) -> e- [] -> ESeq [] Nothing+ -- The environment, with FIRST sets as the transitive closure of the head+ -- graph: exactly what iterating 'exprTy' over whole sets converges to,+ -- because 'exprTy' is a union of its operands' sets under a fixed+ -- nullability. Lazy, and only built when someone looks.+ env = [ (n, Ty (M.findWithDefault False n nullables)+ (S.toAscList (reach (succs n))))+ | n <- names ] - cycleFrom n = go [n] n+ reach = grow S.empty where- go path cur = case [ h | h <- heads cur, h == n ] of- (_:_) -> reverse (n : path)- [] -> case [ p | h <- heads cur- , h `notElem` path- , n `elem` tyFirst (at env h)- , p <- [go (h : path) h]- , not (null p) ] of- (p:_) -> p- [] -> []+ grow seen [] = seen+ grow seen (x:xs)+ | S.member x seen = grow seen xs+ | otherwise = grow (S.insert x seen) (succs x ++ xs)++-- | 'Data.List.nub' in @O(n log n)@, keeping first occurrences in order.+nubOrd :: Ord a => [a] -> [a]+nubOrd = go S.empty+ where+ go _ [] = []+ go seen (x:xs)+ | S.member x seen = go seen xs+ | otherwise = x : go (S.insert x seen) xs -- | Every non-terminal a body references, at any position. refs :: PExpr -> [String]
src/PEG/Grammar.hs view
@@ -5,6 +5,7 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE RankNTypes #-} -- | Grammar and rule-set types. --@@ -30,7 +31,8 @@ -- -- * A 'Rules' chain assembled by hand from 'RCons', or a 'Grammar' built -- around one, is checked for /reference/ errors only. A rule that begins--- with itself compiles, and loops when run.+-- with itself compiles, and loops when run. So is a 'Keyed' grammar whose+-- key type and rules were written by hand. -- * 'PEG.QQ.pegRules' analyses its block open-world, because 'RCons' lets two -- blocks be combined and a name the block does not define may be defined by -- the other one. Left recursion that closes /across/ two blocks is@@ -39,9 +41,10 @@ -- reference is resolved and every cycle is visible. -- -- Prefer 'PEG.QQ.pegGrammar'. It is the only way to write a grammar that is--- checked completely, and it is also the fastest to compile, because it knows--- each rule's position and emits 'PEG.Syntax.ntw' with the membership proof--- rather than a 'PEG.Member.KnownMember' search.+-- checked completely, and it is also the fastest to compile: in declaration+-- position it declares a key type for the grammar, one constructor per rule,+-- and builds a 'Keyed' grammar whose references cost the type checker the+-- same however many rules there are. See "PEG.Key". module PEG.Grammar ( Rules (..) , Grammar (..)@@ -49,10 +52,12 @@ import Data.Kind (Type) +import PEG.Key (InEnv, Tabulate) import PEG.Syntax (Name, PExp) import PEG.Type --- | A typed, heterogeneous list of named grammar rules.+-- | A typed, heterogeneous list of named grammar rules over a type-level+-- environment. -- -- @'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@@ -60,25 +65,37 @@ data Rules (s :: Type) (env :: Env) (defs :: Env) where RNil :: Rules s env '[] RCons :: Name n- -> PExp s env a+ -> PExp s (InEnv env) a -> Rules s env rest -> Rules s env ('(n, 'EnvEntry a) ': rest) -- | A complete PEG grammar over the stream @s@: a set of mutually recursive--- rules and a start expression.------ The @Rules s env env@ field is what ties the two halves together: every--- rule's body is checked against the same environment the rule set defines,--- so a reference can only name a rule that exists and only at the type that--- rule has.+-- rules and a start expression, with its non-terminals named by keys of type+-- @nt@. -- -- 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 a@ — but note that+-- @forall s. 'PEG.Stream.Stream' s => Grammar s (Env s) 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.-data Grammar (s :: Type) (env :: Env) (a :: Type) where+data Grammar (s :: Type) (nt :: Type -> Type) (a :: Type) where+ -- | A grammar over a type-level environment.+ --+ -- The @Rules s env env@ field is what ties the two halves together: every+ -- rule's body is checked against the same environment the rule set+ -- defines, so a reference can only name a rule that exists and only at the+ -- type that rule has. Grammar :: Rules s env env- -> PExp s env a- -> Grammar s env a+ -> PExp s (InEnv env) a+ -> Grammar s (InEnv env) a+ -- | A grammar over a declared key type: the rule each key names, and the+ -- start expression.+ --+ -- The rules are a function rather than a list, so that a rule's body is+ -- found by matching on its key — a @case@ whose every branch is checked on+ -- its own — and not by walking a type-level structure.+ Keyed :: Tabulate nt+ => (forall b. nt b -> PExp s nt b)+ -> PExp s nt a+ -> Grammar s nt a
+ src/PEG/Key.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE RankNTypes #-}++-- | What a non-terminal reference points at.+--+-- A 'PEG.Syntax.PExp' is indexed by a /key type/ @nt :: Type -> Type@: a+-- value of type @nt a@ names a rule returning @a@, and+-- @'PEG.Syntax.NT' :: nt a -> PExp s nt a@ is a reference to it. Checking a+-- reference is then checking the type of one constructor, which costs the+-- type checker the same whatever the size of the grammar.+--+-- There are two kinds of key.+--+-- * A key type declared for one grammar, one constructor per rule:+--+-- @+-- data ArithEnv s a where+-- ArithEnv_expr :: ArithEnv s Exp+-- ArithEnv_term :: ArithEnv s Exp+-- @+--+-- This is what 'PEG.QQ.pegGrammar' generates in declaration position,+-- together with its 'Tabulate' instance, and it is what a grammar of any+-- size should use.+--+-- * @'InEnv' env@, a membership proof into a type-level list of rules+-- ('PEG.Type.Env'). This is what @'PEG.Syntax.nt' \@"expr"@,+-- 'PEG.QQ.pegRules' and a hand-written environment go through.+--+-- == Why keys+--+-- The environment used to be the only index. A reference carried a unary+-- proof, @There (There ... Here)@, of where its rule sits in a type-level+-- list, and the type checker's evidence for that proof is proportional to how+-- deep the rule is times how much of the list is left. Summed over every+-- reference of a grammar, that was the whole of what compiling a large+-- grammar cost: 2.2 GB of heap at 128 rules and more than 8 GB at 256, even+-- with the proof supplied by the splice rather than searched for. A declared+-- key costs 50 MB at 512 rules. See @bench-compile/@.+module PEG.Key+ ( Tabulate (..)+ , Table (..)+ , InEnv (..)+ ) where++import Data.Kind (Type)++import PEG.Member+import PEG.Type++-- | A total function out of a key type, as a value.+newtype Table (nt :: Type -> Type) (f :: Type -> Type) =+ Table { lookupTable :: forall b. nt b -> f b }++-- | A key type whose rules can be enumerated.+--+-- This is what lets a grammar over declared keys be compiled with a knot:+-- 'PEG.Parse.compileGrammar' tabulates the compiled rule bodies once and+-- resolves every reference through the table.+class Tabulate (nt :: Type -> Type) where+ -- | Memoise a function out of the key type.+ --+ -- @'lookupTable' (tabulate f)@ must agree with @f@, and must evaluate+ -- @f k@ at most once for each key @k@ however often it is looked up. The+ -- instance 'PEG.QQ.pegGrammar' generates binds @f k@ for every constructor+ -- in a @let@ outside the lookup.+ tabulate :: (forall b. nt b -> f b) -> Table nt f++ -- | The name of the rule a key refers to.+ ruleName :: nt b -> String++-- | A reference into a type-level environment: a proof that @env@ binds some+-- name to a rule returning @a@.+--+-- The name is not in the type. It does not have to be: a reference is built+-- by 'PEG.Syntax.nt' or 'PEG.Syntax.ntw', which state the name and demand+-- that the environment agrees about the result type.+data InEnv (env :: Env) (a :: Type) where+ InEnv :: Member n env a -> InEnv env a
src/PEG/Member.hs view
@@ -29,8 +29,10 @@ -- "PEG.Type"), so the same discipline is cheap to keep and worth keeping. -- -- Better still is not to search at all: 'PEG.Syntax.ntw' takes the witness--- rather than deriving it, which is what a quasi-quoter emits, since a splice--- knows every rule's position.+-- rather than deriving it, which is what 'PEG.QQ.pegGrammar' emits in+-- expression position, since a splice knows every rule's position. Best is+-- not to have a list: in declaration position 'PEG.QQ.pegGrammar' declares a+-- key type instead, and a reference is a constructor; see "PEG.Key". module PEG.Member ( Member (..) , KnownMember (..)
src/PEG/Parse.hs view
@@ -1,11 +1,11 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DataKinds #-}+{-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE MagicHash #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeAbstractions #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-}@@ -68,10 +68,11 @@ import PEG.CharSet (CharSet, memberCS) import PEG.Grammar import PEG.Indent+import PEG.Key import PEG.Member import PEG.Stream import PEG.Syntax-import PEG.Type+import PEG.Type (Env, EnvEntry (..)) -- | The result of running a grammar. --@@ -132,20 +133,20 @@ } -- | Run a grammar with 'defaultOpts'.-parse :: Stream s => Grammar s env a -> s -> Result s a+parse :: Stream s => Grammar s nt a -> s -> Result s a parse = parseWith defaultOpts {-# INLINABLE parse #-}-{-# SPECIALIZE parse :: Grammar String env a -> String -> Result String a #-}-{-# SPECIALIZE parse :: Grammar T.Text env a -> T.Text -> Result T.Text a #-}+{-# SPECIALIZE parse :: Grammar String nt a -> String -> Result String a #-}+{-# SPECIALIZE parse :: Grammar T.Text nt a -> T.Text -> Result T.Text a #-} {-# SPECIALIZE parse- :: Grammar B.ByteString env a -> B.ByteString -> Result B.ByteString a #-}+ :: Grammar B.ByteString nt a -> B.ByteString -> Result B.ByteString a #-} -- | Run a grammar with custom 'Opts'. -- -- 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 a.- Stream s => Opts -> Grammar s env a -> s -> Result s a+parseWith :: forall s nt a.+ Stream s => Opts -> Grammar s nt a -> s -> Result s a parseWith opts g = run where step = compileGrammar (optTabWidth opts) g@@ -156,11 +157,11 @@ (# | (# a, st #) #) -> OK a (takeS (stOff st) input) (stInput st) {-# INLINABLE parseWith #-} {-# SPECIALIZE parseWith- :: Opts -> Grammar String env a -> String -> Result String a #-}+ :: Opts -> Grammar String nt a -> String -> Result String a #-} {-# SPECIALIZE parseWith- :: Opts -> Grammar T.Text env a -> T.Text -> Result T.Text a #-}+ :: Opts -> Grammar T.Text nt a -> T.Text -> Result T.Text a #-} {-# SPECIALIZE parseWith- :: Opts -> Grammar B.ByteString env a+ :: Opts -> Grammar B.ByteString nt a -> B.ByteString -> Result B.ByteString a #-} --------------------------------------------------------------------------------@@ -170,42 +171,88 @@ -- | 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 '[]+data CRules (s :: Type) (defs :: Env) where+ CNil :: CRules s '[] CCons :: Step s a- -> CRules s env rest- -> CRules s env ('(n, 'EnvEntry a) ': rest)+ -> CRules s rest+ -> CRules s ('(n, 'EnvEntry a) ': rest) -clookup :: Member n defs a -> CRules s env defs -> Step s a+clookup :: Member n defs a -> CRules s defs -> Step s a clookup Here (CCons f _) = f clookup (There m) (CCons _ rest) = clookup m rest +-- | A compiled rule, wrapped so that 'Step' can be the image of a 'Table'.+newtype CStep s a = CStep { unCStep :: Step s a }+ -- | 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 a.- Stream s => Int -> Grammar s env a -> Step s a-compileGrammar tw (Grammar rules start) = compileE tw table start+compileGrammar :: forall s nt a.+ Stream s => Int -> Grammar s nt a -> Step s a+compileGrammar tw (Grammar rules start) = compileListed tw rules start+compileGrammar tw (Keyed rules start) = compileKeyed tw tabulate rules start++-- | A grammar over a type-level environment: the rules are compiled into a+-- list, and a reference is resolved by following its membership proof down+-- that list, once, while the grammar is being compiled.+compileListed :: forall s env a. Stream s+ => Int -> Rules s env env -> PExp s (InEnv env) a -> Step s a+compileListed tw rules start = compileE tw resolve start where- table :: CRules s env env+ table :: CRules s env table = build rules - build :: forall defs. Rules s env defs -> CRules s env defs+ resolve :: forall b. InEnv env b -> Step s b+ resolve (InEnv w) = clookup w table++ build :: forall defs. Rules s env defs -> CRules s defs build RNil = CNil- build (RCons _ body rest) = CCons (compileE tw table body) (build rest)+ build (RCons _ body rest) = CCons (compileE tw resolve body) (build rest)+{-# INLINABLE compileListed #-}++-- | A grammar over declared keys: the compiled rules are tabulated once, and+-- a reference is resolved by looking its key up in the table.+--+-- The knot is safe for the same reason as the list's: looking a key up yields+-- the (lazy) compiled rule without running it, so compiling a rule body never+-- forces the rules it refers to.+--+-- 'tabulate' is an argument rather than a 'Tabulate' constraint, and that is+-- a matter of performance, not of style. GHC specialises a function only on+-- dictionaries it knows, and at the call in 'compileGrammar' the key type is+-- still a variable: with the constraint, the @SPECIALIZE@ pragmas below never+-- reached this function, and a grammar over keys ran the whole parse through+-- the 'Stream' dictionary — on MiniPython, 37% slower and 48% more allocation+-- per character than the same grammar over an environment. With only+-- 'Stream' left to specialise on, the two compile to the same code.+compileKeyed :: forall s nt a. Stream s+ => Int+ -> (forall f. (forall b. nt b -> f b) -> Table nt f)+ -> (forall b. nt b -> PExp s nt b)+ -> PExp s nt a+ -> Step s a+compileKeyed tw tab rules start = compileE tw resolve start+ where+ table :: Table nt (CStep s)+ table = tab (\k -> CStep (compileE tw resolve (rules k)))++ resolve :: forall b. nt b -> Step s b+ resolve k = unCStep (lookupTable table k)+{-# INLINABLE compileKeyed #-}+ {-# 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 a -> Step String a #-}+ :: Int -> Grammar String nt a -> Step String a #-} {-# SPECIALIZE compileGrammar- :: Int -> Grammar T.Text env a -> Step T.Text a #-}+ :: Int -> Grammar T.Text nt a -> Step T.Text a #-} {-# SPECIALIZE compileGrammar- :: Int -> Grammar B.ByteString env a -> Step B.ByteString a #-}+ :: Int -> Grammar B.ByteString nt a -> Step B.ByteString a #-} -- | 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@@ -213,9 +260,9 @@ simpleCS :: CharSet -> Bool simpleCS cs = not (memberCS '\n' cs) && not (memberCS '\t' cs) -compileE :: forall s env a.- Stream s => Int -> CRules s env env -> PExp s env a -> Step s a-compileE tw table = comp+compileE :: forall s nt a. Stream s+ => Int -> (forall b. nt b -> Step s b) -> PExp s nt a -> Step s a+compileE tw resolve = comp where -- Select the stream operations once per compiled grammar. Leaving them -- as class-method applications would repeat the dictionary lookup on@@ -228,7 +275,7 @@ !packS = packString :: String -> s !emptyS = packS [] - comp :: forall b. PExp s env b -> Step s b+ comp :: forall b. PExp s nt b -> Step s b comp (Pure x) = \_ st -> (# | (# x, st #) #) @@ -245,14 +292,9 @@ comp (Span cs) = spanChunk (\c -> memberCS c cs) (simpleCS cs) False comp (Span1 cs) = spanChunk (\c -> memberCS c cs) (simpleCS cs) True - -- 'a' comes from the constructor's own equality- -- @Lookup n env ~ 'EnvEntry a@, so no type family has to be reduced- -- here at all.- comp (NT @n _) = clookup (member @n @env) table-- -- The witness came with the reference, so there is no search at all:- -- neither here nor, more to the point, in the type checker.- comp (NTW _ w) = clookup w table+ -- The key has already been resolved to its compiled rule by whoever+ -- built @resolve@; this is one lookup per occurrence, at compile time.+ comp (NT k) = resolve k -- Neither this nor 'Map' below allocates: the intermediate results travel -- in registers, so a quasi-quoted rule of @n@ items costs @n@ calls and
src/PEG/QQ.hs view
@@ -31,8 +31,10 @@ , pegGrammar ) where -import Control.Monad (foldM)-import Data.List (elemIndex, nub)+import Control.Monad (foldM, unless)+import Data.Data (Data, gmapQ)+import Data.Typeable (cast)+import Data.List (elemIndex, groupBy, nub) import Language.Haskell.TH (Exp (..), Pat (..), Q) import qualified Language.Haskell.TH as TH import Language.Haskell.TH.Quote (QuasiQuoter (..))@@ -40,6 +42,7 @@ import PEG import PEG.Analysis (Diagnostic (..), World (..), analyse, analyseWith, renderDiagnostic, spannable)+import PEG.QQ.Compat (requiredKindedTV, requiredTV) import PEG.QQ.HsExp (parseHsExp, parseHsType) import PEG.QQ.Syntax (Def (..), Directive (..), Item (..), PExpr (..), RelS (..), parseDirectives, parseExpr, parseGrammar,@@ -110,14 +113,16 @@ go (EAlign e) = do e' <- go e [| Align $(pure e') |]- go (EChoice es) = case es of- [] -> fail "QQ: empty choice (should be impossible)"- (e:rest) -> do- e' <- go e- rest' <- mapM go rest- foldM (\acc x -> [| $(pure acc) .||. $(pure x) |]) e' rest'- go (ESeq items act) = translateSeqWith ntRef items act+ go (EChoice es) = do+ alts <- mapM toAlt es+ translateAlts ntRef alts+ go (ESeq items act) = do+ body <- seqBody items act+ translateAlt ntRef (Alt items [] body) + toAlt (ESeq items act) = Alt items [] <$> seqBody items act+ toAlt e = pure (Opaque e)+ -- | Emit @nt \@"name"@: the environment is searched by the type checker. ntByName :: String -> Q Exp ntByName name = pure (TH.AppTypeE (TH.VarE 'nt) (TH.LitT (TH.StrTyLit name)))@@ -130,29 +135,120 @@ translateRel (ROffset n) = [| offsetR n |] translateRel (RNamed nm) = pure (TH.VarE (TH.mkName nm)) -translateSeqWith :: (String -> Q Exp) -> [Item] -> Maybe String -> Q Exp-translateSeqWith ntRef items act = do+-- | One alternative of an ordered choice, on its way to being translated.+--+-- An 'Alt' is the part of a sequence still to be parsed, together with the+-- patterns for values an enclosing factoring has already parsed on its+-- behalf, and the semantic action over all of them. It translates to an+-- expression returning a function of those earlier values: @\rest... ->+-- \outer... -> body@. An alternative that is not a sequence is 'Opaque' and+-- never shares a prefix with anything.+data Alt+ = Alt [Item] [Pat] Exp+ | Opaque PExpr++-- | Translate an ordered choice, factoring out prefixes that consecutive+-- alternatives share.+--+-- == Why+--+-- A PEG does not memoise, so in+--+-- @+-- expr <- e:or_expr ws "if" c:or_expr ws "else" a:expr { ... }+-- / e:or_expr { e }+-- @+--+-- a plain expression is parsed twice: once by the alternative that fails+-- at @"if"@ and once by the one that succeeds. Each precedence level written+-- this way doubles the work, and levels nest — through parentheses, call+-- arguments, list elements — so the cost is exponential in how deeply the+-- /input/ nests. On the MiniPython grammar @print(str(mdc(f(g(x)))))@ took+-- a third of a second, eight times as long per level.+--+-- == What it does+--+-- In a PEG, @A B \/ A C@ and @A (B \/ C)@ accept the same inputs with the+-- same results: @A@ is deterministic, so the second alternative would parse+-- exactly what the first one did before it failed. Consecutive alternatives+-- whose leading items are the same expression — labels aside — are+-- translated as their longest common prefix followed by a choice of what is+-- left of each, which is factored again. Only /consecutive/ alternatives+-- are grouped: in @A B \/ X \/ A C@ the @X@ must still be tried between them.+--+-- Each remainder returns a function of the prefix's values, so that its+-- action still sees the prefix under the labels it gave it.+translateAlts :: (String -> Q Exp) -> [Alt] -> Q Exp+translateAlts ntRef alts = do+ es <- mapM (translateGroup ntRef) (groupBy sameHead alts)+ case es of+ [] -> fail "QQ: empty choice (should be impossible)"+ (e:rest) -> foldM (\acc x -> [| $(pure acc) .||. $(pure x) |]) e rest+ where+ sameHead (Alt (Item _ a : _) _ _) (Alt (Item _ b : _) _ _) = a == b+ sameHead _ _ = False++translateGroup :: (String -> Q Exp) -> [Alt] -> Q Exp+translateGroup ntRef [alt] = translateAlt ntRef alt+translateGroup ntRef grp = do+ let itemss = [ is | Alt is _ _ <- grp ]+ k = commonPrefix itemss+ prefix = [ e | Item _ e <- take k (headItems itemss) ]+ xs <- mapM (\i -> TH.newName ("p" ++ show i)) [1 .. k]+ kf <- TH.newName "rest"+ let apply = LamE (map VarP xs ++ [VarP kf])+ (foldl AppE (VarE kf) (map VarE xs))+ remainders = [ Alt (drop k is) (map itemPat (take k is) ++ outer) body+ | Alt is outer body <- grp ]+ pes <- mapM (translateExprWith ntRef) prefix+ rest <- translateAlts ntRef remainders+ case pes of+ [] -> fail "QQ: factoring an empty prefix (should be impossible)"+ (p:ps) -> do+ hd <- [| fmapP $(pure apply) $(pure p) |]+ foldM (\acc x -> [| $(pure acc) <*>. $(pure x) |]) hd (ps ++ [rest])+ where+ headItems (is:_) = is+ headItems [] = []++ commonPrefix [] = 0+ commonPrefix (i:iss) = foldr (min . agree i) (length i) iss+ agree as bs = length (takeWhile id (zipWith sameItem as bs))+ sameItem (Item _ a) (Item _ b) = a == b++-- | Translate one alternative that nothing is factored out of.+translateAlt :: (String -> Q Exp) -> Alt -> Q Exp+translateAlt ntRef (Opaque e) = translateExprWith ntRef e+translateAlt ntRef (Alt items outer body) = do+ es <- mapM (\(Item _ e) -> translateExprWith ntRef e) items+ case es of+ [] -> [| pureP $(pure (lambda outer body)) |]+ (e:rest) -> do+ hd <- [| fmapP $(pure (LamE (map itemPat items ++ outer) body)) $(pure e) |]+ foldM (\acc x -> [| $(pure acc) <*>. $(pure x) |]) hd rest+ where+ lambda [] b = b+ lambda ps b = LamE ps b++itemPat :: Item -> Pat+itemPat (Item (Just l) _) = VarP (TH.mkName l)+itemPat (Item Nothing _) = WildP++-- | The value a sequence returns: its semantic action, or its labelled items+-- when it has none — one of them bare, several as a tuple, none as @()@.+seqBody :: [Item] -> Maybe String -> Q Exp+seqBody items act = do let labels = [ l | Item (Just l) _ <- items ] case duplicates labels of (l:_) -> fail ("QQ: the label " ++ show l ++ " is used twice in the same sequence") [] -> pure ()- body <- case act of+ case act of Nothing -> pure (defaultBody labels) Just src -> case parseHsExp src of Right e -> pure e Left err -> fail ("QQ: in the semantic action {" ++ src ++ "}: " ++ err)- es <- mapM (\(Item _ e) -> translateExprWith ntRef e) items- case es of- [] -> [| pureP $(pure body) |]- (e:rest) -> do- let pats = zipWith itemPat [1 :: Int ..] items- hd <- [| fmapP $(pure (LamE pats body)) $(pure e) |]- foldM (\acc x -> [| $(pure acc) <*>. $(pure x) |]) hd rest where- itemPat i (Item ml _) =- VarP (TH.mkName (maybe ('_' : show i) id ml))- defaultBody [] = TH.ConE '() defaultBody [l] = TH.VarE (TH.mkName l) defaultBody ls = TH.TupE (map (Just . TH.VarE . TH.mkName) ls)@@ -235,31 +331,14 @@ -- | Quasi-quoter for a complete grammar. -- -- Unlike 'pegRules', which is one part of a rule set and can be combined with--- another, this owns the whole grammar. Two things follow from that.------ It knows every rule's position in the environment, so it emits--- 'PEG.Syntax.ntw' and the membership proof rather than @nt@ and a--- 'PEG.Member.KnownMember' search. That is worth about 2.6x on the compile--- time of a 64-rule grammar; see @bench-compile/@.------ And it knows the whole grammar is in front of it, so a reference to a name--- no rule defines is an error at the splice rather than a type error later.------ == In expression position------ @--- arith :: Stream s => Grammar s ArithEnv _ Exp--- arith = [pegGrammar|--- %start expr--- expr \<- t:term ts:(o:[+-] u:term)* { foldl addOp t ts }--- term \<- ...--- |]--- @+-- another, this owns the whole grammar, so a reference to a name no rule+-- defines is an error at the splice rather than a type error later, and left+-- recursion is looked for across every rule. -- -- == In declaration position ----- Give each rule its result type and the environment need not be written at--- all — the quasi-quoter declares it, along with the grammar and its+-- Give each rule its result type and nothing else need be written — the+-- quasi-quoter declares the grammar's key type, the grammar, and its -- signature: -- -- @@@ -271,24 +350,61 @@ -- |] -- @ ----- declares @type ArithEnv s@, @arith :: Stream s => Grammar s (ArithEnv s) Exp@--- and @arith@ itself. An entry of the environment is a rule's name and the--- type it returns; the type is the one thing the grammar does not determine,--- which is what the annotations are for.+-- declares --+-- @+-- data ArithEnv s a where+-- ArithEnv_expr :: ArithEnv s Exp+-- ArithEnv_term :: ArithEnv s Exp+-- instance 'Tabulate' (ArithEnv s)+--+-- arith'expr :: Stream s => PExp s (ArithEnv s) Exp+-- arith'term :: Stream s => PExp s (ArithEnv s) Exp+--+-- arith :: Stream s => Grammar s (ArithEnv s) Exp+-- @+--+-- A reference to @term@ is @'NT' ArithEnv_term@, whose type is checked+-- without looking at the rest of the grammar, so the cost of type-checking a+-- grammar grows with its size and no faster. Each rule is a binding of its+-- own with the declared type as its signature, so an annotation that+-- disagrees with the rule's body is reported against that rule. The module+-- needs @GADTs@, since the key type is one.+--+-- == In expression position+--+-- @+-- arith :: Grammar String _ Exp+-- arith = [pegGrammar|+-- %start expr+-- expr \<- t:term ts:(o:[+-] u:term)* { foldl addOp t ts }+-- term \<- ...+-- |]+-- @+--+-- An expression cannot declare a type, so there is no key type to generate:+-- the grammar is built over a type-level environment, as 'pegRules' builds+-- it, with each reference carrying its membership proof. That is fine for a+-- grammar of a few dozen rules and increasingly expensive past that; see+-- "PEG.Key". Prefer declaration position, with @%param@ for what the+-- expression form would have captured from its surroundings.+-- -- == Directives -- -- [@%start@] Required. The start expression: a non-terminal's name, or any -- PEG expression over the grammar's rules. -- [@%name@] Required in declaration position: the name to bind the grammar -- to.--- [@%env@] The name of the generated environment synonym. Defaults to the--- grammar's name, capitalised, with @Env@ appended.+-- [@%env@] The name of the generated key type. Defaults to the grammar's+-- name, capitalised, with @Env@ appended. -- [@%stream@] The stream type. Defaults to a variable @s@ with a -- 'PEG.Stream.Stream' constraint. -- [@%result@] The grammar's result type, for the rare start expression whose -- type cannot be read off the rules — one with a semantic action -- of its own.+-- [@%param@] @%param name :: Type@, in declaration position: the grammar and+-- every rule take an argument @name@, in scope in every semantic+-- action. May be repeated; the arguments are taken in order. pegGrammar :: QuasiQuoter pegGrammar = QuasiQuoter { quoteExp = pegGrammarExp@@ -370,13 +486,16 @@ normaliseStart e = e knownDirectives :: [String]-knownDirectives = ["start", "name", "env", "stream", "result"]+knownDirectives = ["start", "name", "env", "stream", "result", "param"] directive :: String -> [Directive] -> Maybe String-directive k ds = case [ v | Directive k' v <- ds, k' == k ] of+directive k ds = case directives k ds of (v:_) -> Just v [] -> Nothing +directives :: String -> [Directive] -> [String]+directives k ds = [ v | Directive k' v <- ds, k' == k ]+ -- | Emit @ntw \@"name" (There (... Here))@: the proof instead of the search. ntByWitness :: [String] -> String -> Q Exp ntByWitness names name = case elemIndex name names of@@ -391,6 +510,9 @@ pegGrammarExp :: String -> Q Exp pegGrammarExp src = do gs <- parseGrammarSrc src+ unless (null (directives "param" (gsDirs gs))) $+ fail "pegGrammar: %param is only meaningful in declaration position;\n\+ \ an expression can use the variables in scope around it" let ntRef = ntByWitness (gsNames gs) rules <- translateRules ntRef (gsDefs gs) start <- translateExprWith ntRef (gsStart gs)@@ -399,47 +521,179 @@ pegGrammarDec :: String -> Q [TH.Dec] pegGrammarDec src = do gs <- parseGrammarSrc src- gname <- case directive "name" (gsDirs gs) of- Just v -> pure (TH.mkName v)+ gadts <- TH.isExtEnabled TH.GADTs+ unless gadts $+ fail "pegGrammar: declaring a grammar declares a GADT, its key type;\n\+ \ enable {-# LANGUAGE GADTs #-} in this module"+ baseName <- case directive "name" (gsDirs gs) of+ Just v -> pure v Nothing -> fail "pegGrammar: no %name directive, which declaring a \ \grammar needs"- let baseName = maybe "" id (directive "name" (gsDirs gs))- envName = TH.mkName (maybe (capitalise baseName ++ "Env") id- (directive "env" (gsDirs gs)))+ let gname = TH.mkName baseName+ envStr = maybe (capitalise baseName ++ "Env") id+ (directive "env" (gsDirs gs))+ envName = TH.mkName envStr streamV = TH.mkName "s"+ polyStream = directive "stream" (gsDirs gs) == Nothing streamT <- case directive "stream" (gsDirs gs) of Nothing -> pure (TH.VarT streamV) Just t -> either (\e -> fail ("pegGrammar: in %stream: " ++ e)) pure (parseHsType t)- anns <- mapM (resultAnnotation gname) (gsDefs gs)- let envRhs = promotedList [ envEntry n ty | (n, ty) <- anns ]+ params <- mapM parseParam (directives "param" (gsDirs gs))+ anns0 <- mapM (resultAnnotation gname) (gsDefs gs)+ -- A result type may mention the stream as @s@. In the key type @s@ is the+ -- type's own parameter, so it can stay; in a signature of a grammar over a+ -- fixed stream it has to become that stream.+ let atStream = if polyStream then id else substVar streamV streamT+ anns = [ (n, atStream t) | (n, t) <- anns0 ] startRes <- case directive "result" (gsDirs gs) of- Just t -> either (\e -> fail ("pegGrammar: in %result: " ++ e)) pure- (parseHsType t)+ Just t -> either (\e -> fail ("pegGrammar: in %result: " ++ e))+ (pure . atStream) (parseHsType t) Nothing -> case resultTypeOf streamT anns (gsStart gs) of Just t -> pure t Nothing -> fail "pegGrammar: cannot tell what the start expression \ \returns.\n It has a semantic action of its own; state \ \its type with %result."- let envApplied = TH.AppT (TH.ConT envName) streamT- grammarTy = foldl TH.AppT (TH.ConT ''Grammar)- [streamT, envApplied, startRes]- sigTy = case directive "stream" (gsDirs gs) of- Just _ -> grammarTy- Nothing -> TH.ForallT [TH.PlainTV streamV TH.SpecifiedSpec]- [TH.AppT (TH.ConT ''Stream) (TH.VarT streamV)]- grammarTy- body <- pegGrammarExp src- pure [ TH.TySynD envName [TH.PlainTV streamV TH.BndrReq] envRhs- , TH.SigD gname sigTy- , TH.FunD gname [TH.Clause [] (TH.NormalB body) []]- ]+ let keyCon n = TH.mkName (envStr ++ "_" ++ n)+ ruleVar n = TH.mkName (baseName ++ "'" ++ n)+ ntT = TH.AppT (TH.ConT envName) streamT+ ntRef n = pure (TH.AppE (TH.ConE 'NT) (TH.ConE (keyCon n)))+ -- A rule that does not use a parameter binds it to @_@, so that a+ -- grammar with a parameter only some actions need compiles cleanly+ -- under @-Wunused-matches@.+ usedPats body = [ if mentions p body then TH.VarP p else TH.WildP+ | (p, _) <- params ]+ paramArgs = [ TH.VarE p | (p, _) <- params ]+ -- @forall s. Stream s => P1 -> ... -> t@, or @P1 -> ... -> t@ over a+ -- fixed stream.+ signature t =+ let body = foldr (\(_, pt) r -> TH.AppT (TH.AppT TH.ArrowT pt) r)+ t params+ in if polyStream+ then TH.ForallT [TH.PlainTV streamV TH.SpecifiedSpec]+ [TH.AppT (TH.ConT ''Stream) (TH.VarT streamV)]+ body+ else body+ names = gsNames gs++ -- The key type: one constructor per rule, indexed by the rule's result.+ resV <- TH.newName "a"+ let keyDecl = TH.DataD [] envName+ [ requiredTV streamV+ , requiredKindedTV resV TH.StarT ]+ Nothing+ [ TH.GadtC [keyCon n] []+ (TH.AppT (TH.AppT (TH.ConT envName) (TH.VarT streamV)) ty)+ | (n, ty) <- anns0 ]+ []++ -- Its 'Tabulate' instance. Every rule's image is bound once, outside the+ -- lookup, which is what makes the table a memo table.+ fV <- TH.newName "f"+ kV <- TH.newName "k"+ xs <- mapM (\n -> TH.newName ("x_" ++ n)) names+ let onKey arms = TH.LamE [TH.VarP kV] (caseOrAbsurd (TH.VarE kV) arms)+ caseOrAbsurd scrut [] =+ -- A grammar with no rules has an uninhabited key type.+ TH.AppE (TH.AppE (TH.VarE 'seq) scrut)+ (TH.AppE (TH.VarE 'error)+ (TH.LitE (TH.StringL "PEG: no rules")))+ caseOrAbsurd scrut arms = TH.CaseE scrut arms+ arm n e = TH.Match (TH.ConP (keyCon n) [] []) (TH.NormalB e) []+ tabulateD = TH.FunD 'tabulate+ [ TH.Clause [TH.VarP fV]+ (TH.NormalB+ (letOrBody+ [ TH.ValD (TH.VarP x)+ (TH.NormalB (TH.AppE (TH.VarE fV)+ (TH.ConE (keyCon n))))+ []+ | (n, x) <- zip names xs ]+ (TH.AppE (TH.ConE 'Table)+ (onKey [ arm n (TH.VarE x)+ | (n, x) <- zip names xs ]))))+ [] ]+ letOrBody [] e = e+ letOrBody ds e = TH.LetE ds e+ ruleNameD = TH.FunD 'ruleName+ [ TH.Clause [TH.VarP kV]+ (TH.NormalB (caseOrAbsurd (TH.VarE kV)+ [ arm n (TH.LitE (TH.StringL n)) | n <- names ]))+ [] ]+ instDecl = TH.InstanceD Nothing []+ (TH.AppT (TH.ConT ''Tabulate)+ (TH.AppT (TH.ConT envName) (TH.VarT streamV)))+ [tabulateD, ruleNameD]++ -- One binding per rule, with the declared type as its signature.+ ruleDecls <- fmap concat $ mapM+ (\(Def n _ e, (_, ty)) -> do+ body <- translateExprWith ntRef e+ pure [ TH.SigD (ruleVar n)+ (signature (foldl TH.AppT (TH.ConT ''PExp) [streamT, ntT, ty]))+ , TH.FunD (ruleVar n)+ [TH.Clause (usedPats body) (TH.NormalB body) []]+ ])+ (zip (gsDefs gs) anns)++ -- The grammar: the rules as a function of their keys, and the start.+ start <- translateExprWith ntRef (gsStart gs)+ let rulesE = onKey [ arm n (foldl TH.AppE (TH.VarE (ruleVar n)) paramArgs)+ | n <- names ]+ grammarTy = foldl TH.AppT (TH.ConT ''Grammar) [streamT, ntT, startRes]+ pure $ [ keyDecl, instDecl ] ++ ruleDecls +++ [ TH.SigD gname (signature grammarTy)+ , TH.FunD gname+ [ TH.Clause (usedPats (TH.AppE rulesE start))+ (TH.NormalB (TH.AppE (TH.AppE (TH.ConE 'Keyed) rulesE) start)) [] ]+ ] where capitalise [] = [] capitalise (c:cs) = toUpper c : cs toUpper c = if c >= 'a' && c <= 'z' then toEnum (fromEnum c - 32) else c --- | A rule's declared result type, which declaring an environment needs.+-- | @%param name :: Type@.+parseParam :: String -> Q (TH.Name, TH.Type)+parseParam src = case breakOnSig src of+ Just (nm, ty)+ | validName nm -> either (\e -> fail ("pegGrammar: in %param " ++ nm+ ++ ": " ++ e))+ (\t -> pure (TH.mkName nm, t))+ (parseHsType ty)+ _ -> fail ("pegGrammar: expected %param name :: Type, found %param " ++ src)+ where+ breakOnSig = go []+ where+ go acc (':':':':rest) = Just (trim (reverse acc), rest)+ go acc (c:cs) = go (c:acc) cs+ go _ [] = Nothing+ trim = reverse . dropWhile (== ' ') . reverse . dropWhile (== ' ')+ validName (c:cs) = (c == '_' || (c >= 'a' && c <= 'z'))+ && all (\x -> x == '_' || x == '\'' || (x >= 'a' && x <= 'z')+ || (x >= 'A' && x <= 'Z')+ || (x >= '0' && x <= '9')) cs+ validName [] = False++-- | Does the name occur anywhere in the expression? Conservative: a+-- binding of the same name inside counts as an occurrence.+mentions :: Data a => TH.Name -> a -> Bool+mentions n x = case cast x of+ Just n' -> n' == n+ Nothing -> or (gmapQ (mentions n) x)++-- | Replace a type variable.+substVar :: TH.Name -> TH.Type -> TH.Type -> TH.Type+substVar v new = go+ where+ go (TH.VarT n) | n == v = new+ go (TH.AppT a b) = TH.AppT (go a) (go b)+ go (TH.AppKindT t k) = TH.AppKindT (go t) k+ go (TH.SigT t k) = TH.SigT (go t) k+ go (TH.InfixT a n b) = TH.InfixT (go a) n (go b)+ go (TH.ParensT t) = TH.ParensT (go t)+ go t = t++-- | A rule's declared result type, which declaring the key type needs. resultAnnotation :: TH.Name -> Def -> Q (String, TH.Type) resultAnnotation gname (Def n ann _) = case ann of Nothing -> fail ("pegGrammar: the rule " ++ n ++ " has no result type.\n\@@ -453,7 +707,7 @@ -- | What the start expression returns, read off the rules' declared types. ----- This follows @translateSeqWith@: a sequence with no semantic action returns+-- This follows @seqBody@: a sequence with no semantic action returns -- its labelled items, one of them bare and several as a tuple. A sequence -- /with/ an action returns whatever the action does, which is Haskell and so -- not knowable here — hence the 'Maybe', and the @%result@ directive.@@ -486,16 +740,3 @@ firstJust xs = case [ x | Just x <- xs ] of (x:_) -> Just x [] -> Nothing------------------------------------------------------------------------------------- Building the environment's type-----------------------------------------------------------------------------------promotedList :: [TH.Type] -> TH.Type-promotedList = foldr (\x acc -> TH.AppT (TH.AppT TH.PromotedConsT x) acc)- TH.PromotedNilT--envEntry :: String -> TH.Type -> TH.Type-envEntry n res =- TH.AppT (TH.AppT (TH.PromotedTupleT 2) (TH.LitT (TH.StrTyLit n)))- (TH.AppT (TH.PromotedT 'EnvEntry) res)
+ src/PEG/QQ/Compat.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE CPP #-}++-- | What differs between the versions of template-haskell the quasi-quoters+-- build against. Kept apart from "PEG.QQ" because CPP does not understand+-- Haskell's string gaps, which that module's error messages use.+module PEG.QQ.Compat+ ( requiredTV+ , requiredKindedTV+ ) where++import qualified Language.Haskell.TH as TH++-- | The binders of a data declaration. template-haskell 2.21 (GHC 9.8) gave+-- them a visibility flag; before it they carry @()@.+#if MIN_VERSION_template_haskell(2,21,0)+requiredTV :: TH.Name -> TH.TyVarBndr TH.BndrVis+requiredTV n = TH.PlainTV n TH.BndrReq++requiredKindedTV :: TH.Name -> TH.Kind -> TH.TyVarBndr TH.BndrVis+requiredKindedTV n k = TH.KindedTV n TH.BndrReq k+#else+requiredTV :: TH.Name -> TH.TyVarBndr ()+requiredTV n = TH.PlainTV n ()++requiredKindedTV :: TH.Name -> TH.Kind -> TH.TyVarBndr ()+requiredKindedTV n k = TH.KindedTV n () k+#endif
src/PEG/QQ/Syntax.hs view
@@ -34,7 +34,7 @@ deriving Show data Item = Item (Maybe String) PExpr- deriving Show+ deriving (Eq, Show) data PExpr = EChoice [PExpr]@@ -52,7 +52,7 @@ | EIndent RelS PExpr | EPos RelS PExpr | EAlign PExpr- deriving Show+ deriving (Eq, Show) data RelS = RGt@@ -61,7 +61,7 @@ | RAny | ROffset Int | RNamed String- deriving Show+ deriving (Eq, Show) type P a = String -> Either String (a, String)
src/PEG/Syntax.hs view
@@ -11,11 +11,12 @@ -- | The PEG expression GADT and combinator API. ----- 'PExp' is the core type: a GADT indexed by the input stream, the grammar--- environment, and the Haskell result type. Every non-terminal reference is--- checked against the environment, so @nt \@\"expr\"@ is a type error unless--- the grammar has a rule called @expr@, and it has whatever type that rule--- has.+-- 'PExp' is the core type: a GADT indexed by the input stream, the type of+-- the grammar's non-terminal keys (see "PEG.Key"), and the Haskell result+-- type. A non-terminal reference is a key, so it can only name a rule that+-- exists and only at the type that rule has: @NT ArithEnv_expr@ for a+-- generated grammar, @nt \@\"expr\"@ for one whose environment is a+-- type-level list. -- -- The first parameter, @s@, is the stream the expression consumes; see -- "PEG.Stream". It appears in the type because a character class produces a@@ -47,8 +48,9 @@ -- lexeme :: PExp s env ty a -> PExp s env (SeqTy ty ('MkTy 'True '[])) a -- @ ----- is now @PExp s env a -> PExp s env a@, and composes without the caller--- having to get a nesting of type families right.+-- is now @PExp s nt a -> PExp s nt a@, and composes without the caller+-- having to get a nesting of type families right. It works unchanged over+-- both kinds of key. module PEG.Syntax ( Name (..) , PExp (..)@@ -75,11 +77,12 @@ ) where import Data.Kind (Type)-import GHC.TypeLits (Symbol, KnownSymbol)+import GHC.TypeLits (Symbol) import PEG.CharSet (CharSet) import qualified PEG.CharSet as CS import PEG.Indent (Rel)+import PEG.Key import PEG.Type import PEG.TyLevel import PEG.Member@@ -98,7 +101,7 @@ -- * '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+-- * 'NT' — invoke a non-terminal, named by its key -- * 'Seq' — sequential composition (@e1 e2@) -- * 'Choice' — ordered choice (@e1 \/ e2@) -- * 'Star' — Kleene star (@e*@)@@ -107,159 +110,136 @@ -- * '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 (s :: Type) (env :: Env) (a :: Type) where- Pure :: a -> PExp s env a- Term :: Char -> PExp s env Char+data PExp (s :: Type) (nt :: Type -> Type) (a :: Type) where+ Pure :: a -> PExp s nt a+ Term :: Char -> PExp s nt 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 Char+ Sat :: !CharSet -> PExp s nt Char -- | Match a string literal. The string must be non-empty; 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 String+ Str :: String -> PExp s nt 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 s+ Span :: !CharSet -> PExp s nt s -- | As 'Span', but the run must be non-empty: @[a-z]+@.- Span1 :: !CharSet -> PExp s env s- AnyChar :: PExp s env Char- -- The environment is looked up /once/, by the equality below, and the- -- result is bound to the rigid variable @a@. Passing @ResOf (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 s env a.- ( KnownSymbol n- , Lookup n env ~ 'EnvEntry a- , KnownMember n env a- )- => Name n- -> PExp s env a- -- | As 'NT', but the proof that the rule is in the environment is supplied- -- rather than searched for.- --- -- The @Lookup@ equality is kept, so this is not a weaker claim than 'NT':- -- @a@ still comes from the environment, and a witness that points at a- -- different rule does not type-check. What is gone is the 'KnownMember'- -- instance chain, which walks the environment one entry at a time for every- -- occurrence of every non-terminal. On a 64-rule grammar that chain is- -- about three quarters of what resolving a reference costs; see- -- @bench-compile/@.- --- -- A splice knows each rule's position and so can write the witness down.- -- Hand-written grammars have nothing to gain here and should keep using- -- 'nt'.- NTW :: forall n s env a.- ( KnownSymbol n- , Lookup n env ~ 'EnvEntry a- )- => Name n- -> Member n env a- -> PExp s env a- Seq :: PExp s env (a -> b)- -> PExp s env a- -> PExp s env b- Choice :: PExp s env a- -> PExp s env a- -> PExp s env a+ Span1 :: !CharSet -> PExp s nt s+ AnyChar :: PExp s nt Char+ -- | A reference to the rule the key names. Its result type is the key's+ -- index, so nothing about the rest of the grammar is consulted.+ NT :: nt a -> PExp s nt a+ Seq :: PExp s nt (a -> b)+ -> PExp s nt a+ -> PExp s nt b+ Choice :: PExp s nt a+ -> PExp s nt a+ -> PExp s nt a -- | Kleene star. The argument must not match the empty string, or the -- parser will not terminate; that is checked by "PEG.Analysis" when the -- grammar is spliced, and not at all when a 'Star' is built by hand.- Star :: PExp s env a- -> PExp s env [a]- Not :: PExp s env a- -> PExp s env ()+ Star :: PExp s nt a+ -> PExp s nt [a]+ Not :: PExp s nt a+ -> PExp s nt () Map :: (a -> b)- -> PExp s env a- -> PExp s env b+ -> PExp s nt a+ -> PExp s nt b Indent :: Rel n- -> PExp s env a- -> PExp s env a+ -> PExp s nt a+ -> PExp s nt a Position :: Rel n- -> PExp s env a- -> PExp s env a- Align :: PExp s env a- -> PExp s env a+ -> PExp s nt a+ -> PExp s nt a+ Align :: PExp s nt a+ -> PExp s nt a -instance Functor (PExp s env) where+instance Functor (PExp s nt) where fmap = Map --- | Reference a non-terminal by name using a type application:--- @nt \@\"ruleName\"@.+-- | Reference a rule of a type-level environment by name, using a type+-- application: @nt \@\"ruleName\"@. -- -- The name is deliberately the /first/ quantified variable, so that -- @nt \@\"expr\"@ keeps working: the stream and environment are recovered by -- unification.+--+-- The environment is searched by the 'KnownMember' instance chain, once for+-- every occurrence, which is what makes a large environment slow to compile.+-- A grammar written with 'PEG.QQ.pegGrammar' in declaration position has+-- declared keys instead, and does not search anything. nt :: forall n env s a.- ( KnownSymbol n- , Lookup n env ~ 'EnvEntry a+ ( Lookup n env ~ 'EnvEntry a , KnownMember n env a )- => PExp s env a-nt = NT (Name :: Name n)+ => PExp s (InEnv env) a+nt = NT (InEnv (member :: Member n env a)) --- | Reference a non-terminal by name, supplying the membership proof:--- @ntw \@"ruleName" (There Here)@.+-- | Reference a rule of a type-level environment by name, supplying the+-- membership proof: @ntw \@"ruleName" (There Here)@. ----- This is what a generated grammar emits; see 'NTW'.+-- The @Lookup@ equality is kept, so this is not a weaker claim than 'nt': @a@+-- still comes from the environment, and a witness that points at a different+-- rule does not type-check. What is gone is the 'KnownMember' search. The+-- proof itself still costs the type checker in proportion to its depth; see+-- "PEG.Key". ntw :: forall n env s a.- ( KnownSymbol n- , Lookup n env ~ 'EnvEntry a+ ( Lookup n env ~ 'EnvEntry a ) => Member n env a- -> PExp s env a-ntw = NTW (Name :: Name n)+ -> PExp s (InEnv env) a+ntw w = NT (InEnv w) -- | Succeed without consuming any input.-pureP :: a -> PExp s env a+pureP :: a -> PExp s nt a pureP = Pure -- | Apply a function to the result of an expression.-fmapP :: (a -> b) -> PExp s env a -> PExp s env b+fmapP :: (a -> b) -> PExp s nt a -> PExp s nt b fmapP = Map -- | Require the sub-expression to satisfy the given column relation.-indent :: Rel n -> PExp s env a -> PExp s env a+indent :: Rel n -> PExp s nt a -> PExp s nt a indent = Indent -- | Override the token mode for the sub-expression.-position :: Rel n -> PExp s env a -> PExp s env a+position :: Rel n -> PExp s nt a -> PExp s nt a position = Position -- | Require the sub-expression to start at the current alignment column.-align :: PExp s env a -> PExp s env a+align :: PExp s nt a -> PExp s nt a align = Align -- | Infix synonym for 'fmapP'.-(<$>.) :: (a -> b) -> PExp s env a -> PExp s env b+(<$>.) :: (a -> b) -> PExp s nt a -> PExp s nt b (<$>.) = Map infixl 4 <$>. -- | Infix sequential composition.-(<*>.) :: PExp s env (a -> b)- -> PExp s env a- -> PExp s env b+(<*>.) :: PExp s nt (a -> b)+ -> PExp s nt a+ -> PExp s nt b (<*>.) = Seq infixl 4 <*>. -- | Sequence two expressions, discarding the result of the first.-(.>>.) :: PExp s env a- -> PExp s env b- -> PExp s env b+(.>>.) :: PExp s nt a+ -> PExp s nt b+ -> PExp s nt b e1 .>>. e2 = Map (\_ b -> b) e1 <*>. e2 infixl 6 .>>. -- | Infix ordered choice (@e1 \/ e2@): try @e1@; if it fails, try @e2@.-(.||.) :: PExp s env a -> PExp s env a -> PExp s env a+(.||.) :: PExp s nt a -> PExp s nt a -> PExp s nt a (.||.) = Choice infixl 5 .||. -- | Optional match: @opt e = (Just \<$\>. e) .||. pureP Nothing@.-opt :: PExp s env a -> PExp s env (Maybe a)+opt :: PExp s nt a -> PExp s nt (Maybe a) opt e = (Just <$>. e) .||. pureP Nothing -- | One-or-more: @plus e = (:) \<$\>. e \<*\>. Star e@.@@ -268,40 +248,40 @@ -- -- 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 a -> PExp s env [a]+plus :: PExp s nt a -> PExp s nt [a] plus e = (:) <$>. e <*>. Star e -- | Match any character of the given set.-sat :: CharSet -> PExp s env Char+sat :: CharSet -> PExp s nt Char sat = Sat -- | Match any character inside one of the given inclusive ranges. -- This is the representation the quasi-quoter emits for @[a-z]@ and -- friends.-charClass :: [(Char, Char)] -> PExp s env Char+charClass :: [(Char, Char)] -> PExp s nt 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 Char+notCharClass :: [(Char, Char)] -> PExp s nt 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 s+spanOf :: CharSet -> PExp s nt s spanOf = Span -- | Match a non-empty run of characters of the set.-spanOf1 :: CharSet -> PExp s env s+spanOf1 :: CharSet -> PExp s nt s spanOf1 = Span1 -- | Match any character in the given list. The list must be non-empty.-oneOf :: [Char] -> PExp s env Char+oneOf :: [Char] -> PExp s nt 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 s env String+stringNE :: String -> PExp s nt String stringNE [] = error "PEG.Syntax.stringNE: empty string" stringNE s = Str s
tests/Analysis.hs view
@@ -48,9 +48,12 @@ import Control.Monad (forM, unless) import Data.List (isPrefixOf, nub, sort, union)+import qualified Data.List as L import System.Exit (exitFailure)+import System.Timeout (timeout)+import Control.Exception (evaluate) -import Data.Proxy (Proxy (..))+import Data.Functor.Const (Const (..)) import PEG import PEG.QQ (pegGrammar)@@ -64,6 +67,7 @@ , "examples/Layout.hs" , "examples/Patterns.hs" , "examples/Compat.hs"+ , "examples/MiniPython.hs" ] main :: IO ()@@ -77,8 +81,8 @@ unless (null failures) exitFailure -- The examples are half the point of this test; a refactor that stops -- finding them must fail rather than pass vacuously.- unless (checked + length failures >= 7) $ do- putStrLn "PEG.Analysis: expected at least 7 grammars in examples/, \+ unless (checked + length failures >= 8) $ do+ putStrLn "PEG.Analysis: expected at least 8 grammars in examples/, \ \found fewer" exitFailure let generated = [ checkDefs ("generated/" ++ show i) g@@ -97,7 +101,9 @@ unless (recursive >= 20 && withHeads >= 20) $ do putStrLn "PEG.Analysis: the generated corpus has gone degenerate" exitFailure+ nested <- nestingCheck let checks = standaloneChecks ++ [witnessCheck] ++ generatedChecks+ ++ factoringChecks ++ [nested] mapM_ report checks unless (all (\(_, ok) -> ok) checks) exitFailure where@@ -169,7 +175,7 @@ go (EIndent _ e) = go e go (EPos _ e) = go e go (EAlign e) = go e- go (EChoice es) = foldl' union [] (map go es)+ go (EChoice es) = L.foldl' union [] (map go es) go (ESeq its _) = seqHeads [ e | Item _ e <- its ] seqHeads [] = []@@ -296,7 +302,7 @@ seeds = iterate (\x -> (x * 1103515245 + 12345) `mod` 2147483648) 1 grammarFrom :: Int -> [Def]-grammarFrom seed0 = snd (foldl' rule (seed0, []) [0 .. n - 1])+grammarFrom seed0 = snd (L.foldl' rule (seed0, []) [0 .. n - 1]) where n = 2 + seed0 `mod` 4 names = [ "r" ++ show i | i <- [0 .. n - 1] ]@@ -398,10 +404,10 @@ , '("digits", 'EnvEntry Int) ] -digitsCount :: PExp String NtwEnv Int+digitsCount :: PExp String (InEnv NtwEnv) Int digitsCount = fmapP (length . chunkToString) (spanOf1 (fromRanges [('0', '9')])) -ntwGrammar :: Grammar String NtwEnv (Int, Int)+ntwGrammar :: Grammar String (InEnv NtwEnv) (Int, Int) ntwGrammar = Grammar (RCons (Name @"pair")@@ -423,32 +429,133 @@ digits :: Int <- ds:[0-9]+ { length (chunkToString ds) } |] --- The environment a reader would have written for that grammar. GHC has--- already agreed that the generated one is well-formed — it type-checked--- @tiny@ — so what this pins down is that it is also the /expected/ one: same--- rules, same order, same spelling.-type ExpectedTinyEnv s =- '[ '("pair" , 'EnvEntry (Int, Int))- , '("digits", 'EnvEntry Int)- ]+-- The key type a reader would have written for that grammar: one constructor+-- per rule, named after the rule, at the rule's declared type. GHC checks+-- these signatures against the generated declarations, so what this pins down+-- is that the generated key type is also the /expected/ one — same rules,+-- same spelling, same types — and so are the per-rule bindings.+expectedKeys :: (TinyEnv String (Int, Int), TinyEnv String Int)+expectedKeys = (TinyEnv_pair, TinyEnv_digits) -sameEnv :: forall (a :: Env) (b :: Env). (a ~ b) => Proxy a -> Proxy b -> ()-sameEnv _ _ = ()+expectedBindings :: ( PExp String (TinyEnv String) (Int, Int)+ , PExp String (TinyEnv String) Int )+expectedBindings = (tiny'pair, tiny'digits) -generatedEnvIsExpected :: ()-generatedEnvIsExpected =- sameEnv (Proxy :: Proxy (TinyEnv String))- (Proxy :: Proxy (ExpectedTinyEnv String))+-- A grammar over a fixed stream, with a parameter, whose result types mention+-- the stream as @s@: the one place a result type has to be rewritten, since+-- the stream is no longer a variable.+[pegGrammar|+ %name scaled+ %stream String+ %param factor :: Int+ %start num + num :: Int <- ds:digits { factor * length ds }+ digits :: s <- ds:[0-9]++|]+ generatedChecks :: [(String, Bool)] generatedChecks =- [ ("pegGrammar generates the expected environment",- generatedEnvIsExpected == ())+ [ ("pegGrammar generates the expected keys",+ case expectedKeys of+ (p, d) -> ruleName p == "pair" && ruleName d == "digits")+ , ("pegGrammar generates the expected rule bindings",+ case expectedBindings of (_, _) -> True)+ , ("a generated key table agrees with the function it tabulates",+ [ getConst (lookupTable table TinyEnv_pair)+ , getConst (lookupTable table TinyEnv_digits) ] == ["pair", "digits"]) , ("a generated grammar parses", case parse tiny "12,345" of OK r _ rest -> r == (2, 3) && rest == "" Fail -> False)+ , ("%param and %stream: a result type of s becomes the stream",+ case parse (scaled 10) "123" of+ OK r _ rest -> r == 30 && rest == ""+ Fail -> False) ]+ where+ table :: Table (TinyEnv String) (Const String)+ table = tabulate (\k -> Const (ruleName k))++--------------------------------------------------------------------------------+-- Factoring shared prefixes out of an ordered choice+--------------------------------------------------------------------------------++-- The translation turns @A B / A C@ into @A (B / C)@. What can go wrong is+-- which alternative wins and what its action sees, so this grammar has:+--+-- * a group of alternatives sharing one item, under a different label in+-- each, whose remainders share two more items and are factored again;+-- * an alternative that is all prefix, so that its remainder is empty;+-- * two alternatives with the same prefix that are /not/ consecutive, which+-- must not be grouped, since the one between them has to be tried first;+-- * two identical alternatives, of which the first must win;+-- * unlabelled prefixes and sequences with no action, whose value is their+-- labels.+[pegGrammar|+ %name shared+ %stream String+ %start top++ top :: [String] <- ss:(s:stmt ';')* !.++ stmt :: String <-+ 'x' "yz" { "xyz" }+ / n:ident "+=" v:num { n ++ " add " ++ v }+ / m:ident "-=" w:num { m ++ " sub " ++ w }+ / p:ident ':' q:ident '=' r:num { p ++ ":" ++ q ++ " set " ++ r }+ / p2:ident ':' q2:ident { p2 ++ ":" ++ q2 }+ / k:ident '=' u:num { k ++ " set " ++ u }+ / j:ident { "bare " ++ j }+ / 'x' 'q' { "unreachable" }+ / '#' t:tag { t }+ / '@' { "first" }+ / '@' { "second" }++ tag :: String <- '<' a:ident '>' / '<' b:num++ ident :: s <- cs:[a-z]++ num :: s <- ds:[0-9]++|]++factoringChecks :: [(String, Bool)]+factoringChecks =+ [ ("factoring keeps which alternative wins and what its action sees",+ parse shared "a+=1;b-=2;c:d=3;e:f;g=4;h;xyz;xq;#<i>;#<5;@;"+ `okWith` [ "a add 1", "b sub 2", "c:d set 3", "e:f", "g set 4"+ , "bare h", "xyz", "bare xq", "i", "5", "first" ])+ , ("factoring keeps a failure a failure",+ case parse shared "a+=;" of+ Fail -> True+ _ -> False)+ ]+ where+ okWith (OK r _ rest) want = r == want && rest == ""+ okWith Fail _ = False++-- Every level of this grammar would parse its operand twice without+-- factoring — once by the alternative that fails at '+', once by the one+-- that succeeds — so forty nested parentheses would take 2^40 steps. With+-- it they take forty. The time limit turns a regression into a failure+-- instead of a hang.+[pegGrammar|+ %name nest+ %stream String+ %start e++ e :: Int <- a:atom '+' b:e { a + b } / a:atom { a }+ atom :: Int <- '(' x:e ')' { x } / ds:[0-9]+ { length ds }+|]++nestingCheck :: IO (String, Bool)+nestingCheck = do+ let depth = 40 :: Int+ input = replicate depth '(' ++ "1+22" ++ replicate depth ')'+ r <- timeout 5000000 (evaluate (case parse nest input of+ OK n _ rest -> n == 3 && rest == ""+ Fail -> False))+ pure ("a shared prefix is parsed once, not once per alternative",+ r == Just True) witnessCheck :: (String, Bool) witnessCheck =
typed-peg.cabal view
@@ -1,14 +1,15 @@ cabal-version: 3.0 name: typed-peg-version: 0.3.0.0+version: 0.4.0.0 synopsis: Type-safe PEG parser combinators description: A library for building Parsing Expression Grammars parsers- with compile-time safety guarantees. Non-terminal references are- checked at the type level against an environment binding each rule- to the type it returns; left recursion, a repetition that cannot- consume input, an undefined non-terminal and a duplicate rule are- reported when the grammar is spliced, naming the rule.+ with compile-time safety guarantees. A non-terminal reference is a+ key whose type is the result of the rule it names, so references are+ checked by the type checker in time linear in the size of the grammar;+ left recursion, a repetition that cannot consume input, an undefined+ non-terminal and a duplicate rule are reported when the grammar is+ spliced, naming the rule. . A quasi-quoter (@PEG.QQ@) allows writing grammars in a concrete DSL syntax. Indentation-sensitive parsing is supported natively@@ -31,7 +32,7 @@ extra-doc-files: CHANGELOG.md peg-patterns.md-tested-with: GHC == 9.10.3+tested-with: GHC == 9.6.6 || == 9.10.3 source-repository head type: git@@ -65,6 +66,7 @@ PEG.CharSet PEG.Grammar PEG.Indent+ PEG.Key PEG.Member PEG.Parse PEG.QQ@@ -75,9 +77,12 @@ PEG.Syntax PEG.TyLevel PEG.Type+ other-modules:+ PEG.QQ.Compat build-depends: base >= 4.18 && < 5 , bytestring >= 0.11 && < 0.13+ , containers >= 0.6 && < 0.8 , template-haskell >= 2.19 && < 2.24 , text >= 2.0 && < 2.2 @@ -86,7 +91,7 @@ type: exitcode-stdio-1.0 hs-source-dirs: examples main-is: Main.hs- other-modules: Arith, Layout, Compat, Patterns+ other-modules: Arith, Layout, Compat, Patterns, MiniPython build-depends: base , bytestring