packages feed

typed-peg 0.2.0.0 → 0.3.0.0

raw patch · 21 files changed

+2394/−902 lines, 21 filesdep ~basePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: base

API changes (from Hackage documentation)

- PEG.Grammar: type family Acyclic (env :: Env)
- PEG.Member: instance (s GHC.Types.~ t) => PEG.Member.KnownMemberStep 'GHC.Types.EQ s ('(t, 'PEG.Type.EnvEntry ty a) : rest) a
- PEG.QQ: instance GHC.Show.Show PEG.QQ.Def
- PEG.QQ: instance GHC.Show.Show PEG.QQ.Item
- PEG.QQ: instance GHC.Show.Show PEG.QQ.PExpr
- PEG.QQ: instance GHC.Show.Show PEG.QQ.RelS
- PEG.Syntax: instance GHC.Base.Functor (PEG.Syntax.PExp s env ty)
- PEG.Syntax: type ChoiceTy (t1 :: Ty) (t2 :: Ty) = 'MkTy Or Nullable t1 Nullable t2 Union First t1 First t2
- PEG.Syntax: type NTTy (n :: Symbol) (env :: Env) = NTGo n TyOf Lookup n env
- PEG.Syntax: type SeqTy (t1 :: Ty) (t2 :: Ty) = 'MkTy And Nullable t1 Nullable t2 Union First t1 If Nullable t1 First t2 '[] :: [Symbol]
- PEG.Syntax: type family NTGo (n :: Symbol) (t :: Ty) :: Ty
- PEG.Type: MkTy :: Bool -> [Symbol] -> Ty
- PEG.Type: data Ty
+ PEG.Analysis: Closed :: World
+ PEG.Analysis: DuplicateRule :: String -> Diagnostic
+ PEG.Analysis: LeftRecursive :: String -> [String] -> Diagnostic
+ PEG.Analysis: NullableStar :: String -> Diagnostic
+ PEG.Analysis: Open :: World
+ PEG.Analysis: Ty :: !Bool -> ![String] -> Ty
+ PEG.Analysis: UndefinedNT :: String -> [String] -> Diagnostic
+ PEG.Analysis: [tyFirst] :: Ty -> ![String]
+ PEG.Analysis: [tyNullable] :: Ty -> !Bool
+ PEG.Analysis: analyse :: [Def] -> Either [Diagnostic] RuleEnv
+ PEG.Analysis: analyseWith :: World -> [Def] -> Either [Diagnostic] RuleEnv
+ PEG.Analysis: choiceTy :: Ty -> Ty -> Ty
+ PEG.Analysis: data Diagnostic
+ PEG.Analysis: data Ty
+ PEG.Analysis: data World
+ PEG.Analysis: exprTy :: (String -> Ty) -> PExpr -> Ty
+ PEG.Analysis: insertSym :: String -> [String] -> [String]
+ PEG.Analysis: instance GHC.Classes.Eq PEG.Analysis.Diagnostic
+ PEG.Analysis: instance GHC.Classes.Eq PEG.Analysis.Ty
+ PEG.Analysis: instance GHC.Classes.Eq PEG.Analysis.World
+ 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.Analysis: renderDiagnostic :: Diagnostic -> String
+ PEG.Analysis: renderEnv :: (String -> String) -> RuleEnv -> String
+ PEG.Analysis: seqTy :: Ty -> Ty -> Ty
+ PEG.Analysis: spannable :: PExpr -> Bool
+ PEG.Analysis: type RuleEnv = [(String, Ty)]
+ PEG.Analysis: unionSym :: [String] -> [String] -> [String]
+ PEG.Member: instance (s GHC.Types.~ t) => PEG.Member.KnownMemberStep 'GHC.Types.EQ s ('(t, 'PEG.Type.EnvEntry a) : rest) a
+ PEG.QQ: pegGrammar :: QuasiQuoter
+ PEG.QQ.HsExp: parseHsType :: String -> Either String Type
+ PEG.QQ.Syntax: Def :: String -> Maybe String -> PExpr -> Def
+ PEG.QQ.Syntax: Directive :: String -> String -> Directive
+ PEG.QQ.Syntax: EAlign :: PExpr -> PExpr
+ PEG.QQ.Syntax: EAnd :: PExpr -> PExpr
+ PEG.QQ.Syntax: EChar :: Char -> PExpr
+ PEG.QQ.Syntax: EChoice :: [PExpr] -> PExpr
+ PEG.QQ.Syntax: EClass :: Bool -> [(Char, Char)] -> PExpr
+ PEG.QQ.Syntax: EDot :: PExpr
+ PEG.QQ.Syntax: EIndent :: RelS -> PExpr -> PExpr
+ PEG.QQ.Syntax: ENT :: String -> PExpr
+ PEG.QQ.Syntax: ENot :: PExpr -> PExpr
+ PEG.QQ.Syntax: EOpt :: PExpr -> PExpr
+ PEG.QQ.Syntax: EPlus :: PExpr -> PExpr
+ PEG.QQ.Syntax: EPos :: RelS -> PExpr -> PExpr
+ PEG.QQ.Syntax: ESeq :: [Item] -> Maybe String -> PExpr
+ PEG.QQ.Syntax: EStar :: PExpr -> PExpr
+ PEG.QQ.Syntax: EString :: String -> PExpr
+ PEG.QQ.Syntax: Item :: Maybe String -> PExpr -> Item
+ PEG.QQ.Syntax: RAny :: RelS
+ PEG.QQ.Syntax: REq :: RelS
+ PEG.QQ.Syntax: RGe :: RelS
+ PEG.QQ.Syntax: RGt :: RelS
+ PEG.QQ.Syntax: RNamed :: String -> RelS
+ PEG.QQ.Syntax: ROffset :: Int -> RelS
+ PEG.QQ.Syntax: data Def
+ PEG.QQ.Syntax: data Directive
+ PEG.QQ.Syntax: data Item
+ PEG.QQ.Syntax: data PExpr
+ PEG.QQ.Syntax: data RelS
+ 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.QQ.Syntax: parseDirectives :: P [Directive]
+ PEG.QQ.Syntax: parseExpr :: P PExpr
+ PEG.QQ.Syntax: parseGrammar :: P [Def]
+ PEG.QQ.Syntax: spaces :: String -> String
+ PEG.QQ.Syntax: type P a = String -> Either String (a, String)
+ 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.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.Grammar: [Grammar] :: forall (env :: Env) s (startTy :: Ty) startA. Acyclic env => Rules s env env -> PExp s env startTy startA -> Grammar s env startTy startA
+ PEG.Grammar: [Grammar] :: forall s (env :: Env) a. Rules s env env -> PExp s env a -> Grammar s env a
- PEG.Grammar: [RCons] :: forall (n :: Symbol) s (env :: Env) (ty :: Ty) a (rest :: [(Symbol, EnvEntry)]). Name n -> PExp s env ty a -> Rules s env rest -> Rules s env ('(n, 'EnvEntry ty a) ': rest)
+ PEG.Grammar: [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: data Grammar s (env :: Env) (startTy :: Ty) startA
+ PEG.Grammar: data Grammar s (env :: Env) a
- PEG.Member: [Here] :: forall (s :: Symbol) (ty :: Ty) a (rest :: [(Symbol, EnvEntry)]). Member s ('(s, 'EnvEntry ty a) ': rest) a
+ PEG.Member: [Here] :: forall (s :: Symbol) a (rest :: [(Symbol, EnvEntry)]). Member s ('(s, 'EnvEntry a) ': rest) a
- PEG.Parse: compileGrammar :: forall s (env :: Env) (ty :: Ty) a. Stream s => Int -> Grammar s env ty a -> Step s a
+ PEG.Parse: compileGrammar :: forall s (env :: Env) a. Stream s => Int -> Grammar s env a -> Step s a
- PEG.Parse: parse :: forall s (env :: Env) (ty :: Ty) a. Stream s => Grammar s env ty a -> s -> Result s a
+ PEG.Parse: parse :: forall s (env :: Env) a. Stream s => Grammar s env a -> s -> Result s a
- PEG.Parse: parseWith :: forall s (env :: Env) (ty :: Ty) a. Stream s => Opts -> Grammar s env ty 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.Syntax: (.>>.) :: forall s (env :: Env) (t1 :: Ty) a (t2 :: Ty) b. PExp s env t1 a -> PExp s env t2 b -> PExp s env (SeqTy t1 t2) b
+ PEG.Syntax: (.>>.) :: forall s (env :: Env) a b. PExp s env a -> PExp s env b -> PExp s env b
- PEG.Syntax: (.||.) :: forall s (env :: Env) (t1 :: Ty) a (t2 :: Ty). PExp s env t1 a -> PExp s env t2 a -> PExp s env (ChoiceTy t1 t2) a
+ PEG.Syntax: (.||.) :: forall s (env :: Env) a. PExp s env a -> PExp s env a -> PExp s env a
- PEG.Syntax: (<$>.) :: forall a b s (env :: Env) (ty :: Ty). (a -> b) -> PExp s env ty a -> PExp s env ty b
+ PEG.Syntax: (<$>.) :: forall a b s (env :: Env). (a -> b) -> PExp s env a -> PExp s env b
- PEG.Syntax: (<*>.) :: forall s (env :: Env) (t1 :: Ty) a b (t2 :: Ty). PExp s env t1 (a -> b) -> PExp s env t2 a -> PExp s env (SeqTy t1 t2) b
+ PEG.Syntax: (<*>.) :: forall s (env :: Env) a b. PExp s env (a -> b) -> PExp s env a -> PExp s env b
- PEG.Syntax: [Align] :: forall s (env :: Env) (ty :: Ty) a. PExp s env ty a -> PExp s env ty a
+ PEG.Syntax: [Align] :: forall s (env :: Env) a. PExp s env a -> PExp s env a
- PEG.Syntax: [AnyChar] :: forall s (env :: Env). PExp s env ('MkTy 'False ('[] :: [Symbol])) Char
+ PEG.Syntax: [AnyChar] :: forall s (env :: Env). PExp s env Char
- PEG.Syntax: [Choice] :: forall s (env :: Env) (t1 :: Ty) a (t2 :: Ty). PExp s env t1 a -> PExp s env t2 a -> PExp s env ('MkTy (Or (Nullable t1) (Nullable t2)) (Union (First t1) (First t2))) a
+ PEG.Syntax: [Choice] :: forall s (env :: Env) a. PExp s env a -> PExp s env a -> PExp s env a
- PEG.Syntax: [Indent] :: forall (n :: Symbol) s (env :: Env) (ty :: Ty) a. Rel n -> PExp s env ty a -> PExp s env ty a
+ PEG.Syntax: [Indent] :: forall (n :: Symbol) s (env :: Env) a. Rel n -> PExp s env a -> PExp s env a
- PEG.Syntax: [Map] :: forall a1 a s (env :: Env) (ty :: Ty). (a1 -> a) -> PExp s env ty a1 -> PExp s env ty a
+ PEG.Syntax: [Map] :: forall a1 a s (env :: Env). (a1 -> a) -> PExp s env a1 -> PExp s env a
- PEG.Syntax: [NT] :: forall (n :: Symbol) (ty1 :: Ty) s (env :: Env) a. (KnownSymbol n, Lookup n env ~ 'EnvEntry ty1 a, KnownMember n env a) => Name n -> PExp s env (NTGo n ty1) a
+ PEG.Syntax: [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: [Not] :: forall s (env :: Env) (n :: Bool) (f :: [Symbol]) a1. PExp s env ('MkTy n f) a1 -> PExp s env ('MkTy 'True f) ()
+ PEG.Syntax: [Not] :: forall s (env :: Env) a1. PExp s env a1 -> PExp s env ()
- PEG.Syntax: [Position] :: forall (n :: Symbol) s (env :: Env) (ty :: Ty) a. Rel n -> PExp s env ty a -> PExp s env ty a
+ PEG.Syntax: [Position] :: forall (n :: Symbol) s (env :: Env) a. Rel n -> PExp s env a -> PExp s env a
- PEG.Syntax: [Pure] :: forall a s (env :: Env). a -> PExp s env ('MkTy 'True ('[] :: [Symbol])) a
+ PEG.Syntax: [Pure] :: forall a s (env :: Env). a -> PExp s env a
- PEG.Syntax: [Sat] :: forall s (env :: Env). !CharSet -> PExp s env ('MkTy 'False ('[] :: [Symbol])) Char
+ PEG.Syntax: [Sat] :: forall s (env :: Env). !CharSet -> PExp s env Char
- PEG.Syntax: [Seq] :: forall s (env :: Env) (t1 :: Ty) a1 a (t2 :: Ty). PExp s env t1 (a1 -> a) -> PExp s env t2 a1 -> PExp s env ('MkTy (And (Nullable t1) (Nullable t2)) (Union (First t1) (If (Nullable t1) (First t2) ('[] :: [Symbol])))) a
+ PEG.Syntax: [Seq] :: forall s (env :: Env) a1 a. PExp s env (a1 -> a) -> PExp s env a1 -> PExp s env a
- PEG.Syntax: [Span1] :: forall s (env :: Env). !CharSet -> PExp s env ('MkTy 'False ('[] :: [Symbol])) s
+ PEG.Syntax: [Span1] :: forall s (env :: Env). !CharSet -> PExp s env s
- PEG.Syntax: [Span] :: forall s (env :: Env). !CharSet -> PExp s env ('MkTy 'True ('[] :: [Symbol])) s
+ PEG.Syntax: [Span] :: forall s (env :: Env). !CharSet -> PExp s env s
- PEG.Syntax: [Star] :: forall s (env :: Env) (f :: [Symbol]) a1. PExp s env ('MkTy 'False f) a1 -> PExp s env ('MkTy 'True f) [a1]
+ PEG.Syntax: [Star] :: forall s (env :: Env) a1. PExp s env a1 -> PExp s env [a1]
- PEG.Syntax: [Str] :: forall s (env :: Env). String -> PExp s env ('MkTy 'False ('[] :: [Symbol])) [Char]
+ PEG.Syntax: [Str] :: forall s (env :: Env). String -> PExp s env [Char]
- PEG.Syntax: [Term] :: forall s (env :: Env). Char -> PExp s env ('MkTy 'False ('[] :: [Symbol])) Char
+ PEG.Syntax: [Term] :: forall s (env :: Env). Char -> PExp s env Char
- PEG.Syntax: align :: forall s (env :: Env) (ty :: Ty) a. PExp s env ty a -> PExp s env ty a
+ PEG.Syntax: align :: forall s (env :: Env) a. PExp s env a -> PExp s env a
- PEG.Syntax: charClass :: forall s (env :: Env). [(Char, Char)] -> PExp s env ('MkTy 'False ('[] :: [Symbol])) Char
+ PEG.Syntax: charClass :: forall s (env :: Env). [(Char, Char)] -> PExp s env Char
- PEG.Syntax: data PExp s (env :: Env) (ty :: Ty) a
+ PEG.Syntax: data PExp s (env :: Env) a
- PEG.Syntax: fmapP :: forall a b s (env :: Env) (ty :: Ty). (a -> b) -> PExp s env ty a -> PExp s env ty b
+ PEG.Syntax: fmapP :: forall a b s (env :: Env). (a -> b) -> PExp s env a -> PExp s env b
- PEG.Syntax: indent :: forall (n :: Symbol) s (env :: Env) (ty :: Ty) a. Rel n -> PExp s env ty a -> PExp s env ty a
+ PEG.Syntax: indent :: forall (n :: Symbol) s (env :: Env) a. Rel n -> PExp s env a -> PExp s env a
- PEG.Syntax: notCharClass :: forall s (env :: Env). [(Char, Char)] -> PExp s env ('MkTy 'False ('[] :: [Symbol])) Char
+ PEG.Syntax: notCharClass :: forall s (env :: Env). [(Char, Char)] -> PExp s env Char
- PEG.Syntax: nt :: forall (n :: Symbol) (env :: Env) s (ty :: Ty) a. (KnownSymbol n, Lookup n env ~ 'EnvEntry ty a, KnownMember n env a) => PExp s env (NTGo n ty) a
+ PEG.Syntax: 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: oneOf :: forall s (env :: Env). [Char] -> PExp s env ('MkTy 'False ('[] :: [Symbol])) Char
+ PEG.Syntax: oneOf :: forall s (env :: Env). [Char] -> PExp s env Char
- PEG.Syntax: opt :: forall s (env :: Env) (t :: Ty) a. PExp s env t a -> PExp s env (ChoiceTy t ('MkTy 'True ('[] :: [Symbol]))) (Maybe a)
+ PEG.Syntax: opt :: forall s (env :: Env) a. PExp s env a -> PExp s env (Maybe a)
- PEG.Syntax: plus :: forall s (env :: Env) (f :: [Symbol]) a. PExp s env ('MkTy 'False f) a -> PExp s env (SeqTy ('MkTy 'False f) ('MkTy 'True f)) [a]
+ PEG.Syntax: plus :: forall s (env :: Env) a. PExp s env a -> PExp s env [a]
- PEG.Syntax: position :: forall (n :: Symbol) s (env :: Env) (ty :: Ty) a. Rel n -> PExp s env ty a -> PExp s env ty a
+ PEG.Syntax: position :: forall (n :: Symbol) s (env :: Env) a. Rel n -> PExp s env a -> PExp s env a
- PEG.Syntax: pureP :: forall a s (env :: Env). a -> PExp s env ('MkTy 'True ('[] :: [Symbol])) a
+ PEG.Syntax: pureP :: forall a s (env :: Env). a -> PExp s env a
- PEG.Syntax: sat :: forall s (env :: Env). CharSet -> PExp s env ('MkTy 'False ('[] :: [Symbol])) Char
+ PEG.Syntax: sat :: forall s (env :: Env). CharSet -> PExp s env Char
- PEG.Syntax: spanOf :: forall s (env :: Env). CharSet -> PExp s env ('MkTy 'True ('[] :: [Symbol])) s
+ PEG.Syntax: spanOf :: forall s (env :: Env). CharSet -> PExp s env s
- PEG.Syntax: spanOf1 :: forall s (env :: Env). CharSet -> PExp s env ('MkTy 'False ('[] :: [Symbol])) s
+ PEG.Syntax: spanOf1 :: forall s (env :: Env). CharSet -> PExp s env s
- PEG.Syntax: stringNE :: forall s (env :: Env). String -> PExp s env ('MkTy 'False ('[] :: [Symbol])) String
+ PEG.Syntax: stringNE :: forall s (env :: Env). String -> PExp s env String
- PEG.Type: EnvEntry :: Ty -> Type -> EnvEntry
+ PEG.Type: EnvEntry :: Type -> EnvEntry

Files

CHANGELOG.md view
@@ -1,6 +1,208 @@ # Changelog -## Unreleased — compile time of large grammars+## Unreleased++### Breaking. The environment no longer carries FIRST sets++An entry of a grammar's environment was a rule's nullability, its FIRST set+and its result type.  It is now the result type:++```haskell+type CalcEnv =+  '[ '("expr" , 'EnvEntry Expr)     -- was 'EnvEntry ('MkTy 'False '["atom", "term", "unary"]) Expr+   , '("term" , 'EnvEntry Expr)+   , '("atom" , 'EnvEntry Expr)+   ]+```++`PExp` loses its `ty` index and is now `PExp s env a`; `Grammar` is+`Grammar s env a`.  `PEG.Type.Ty`, `Nullable`, `First`, `TyOf`,+`PEG.Syntax.SeqTy`, `ChoiceTy`, `NTTy`, `NTGo`, `PEG.Grammar.Acyclic` and the+sorted-set families in `PEG.TyLevel` — `Union`, `Elem`, `ConsIfAbsent`, `If`,+`And`, `Or`, `SymEq` — are gone.  A grammar written with `pegGrammar` needs no+change; one that writes its environment by hand needs the `'MkTy` component+deleted from each entry and the `ty` argument deleted from its signatures.++**Why.**  The FIRST sets in the environment were the entire cost of compiling+a large grammar, and the measurement that says so is that *not computing them+was worth nothing*.  A mode that kept the large environment but handed GHC+every rule's index as a literal, so that `SeqTy`, `ChoiceTy` and `Union` were+never reduced, ran no faster than one that reduced them all.  What cost was+the environment being `O(N^2)` type nodes and each of the `2N` reference+constraints being solved against it: in the micro-benchmark, giving each entry+a payload that no type family ever reads takes `N = 64` from 0.77 s to 11.8 s+and exhausts 8 GB at `N = 96`.++**What it bought**, on a grammar of `N` mutually referring rules+(`bench-compile/`, `ghc -fno-code`):++| N | before | after | |+|---|---|---|---|+| 64, environment by hand | 15.15 s | 1.99 s | 7.6x |+| 64, through `pegGrammar` | 7.23 s | 1.19 s | 6.1x |+| 128, through `pegGrammar` | — | 5.28 s | |++The curve changed and not only the constant: doubling the grammar from 32 to+64 rules used to cost about 8x and now costs 3.3x, so what was cubic in the+number of rules is closer to quadratic.  A 128-rule grammar through+`pegGrammar` now costs less than a 64-rule one did.++Two shapes that used to differ by 4.9x — a rule beginning with a non-terminal+against one beginning with a terminal — are now indistinguishable, which is+the check that the cost is gone rather than moved.++**Leaving the environment to inference now works.**  `Grammar s _ a` with a+wildcard environment used to be unusable: GHC inferred entries full of+unreduced type-family applications and was past 24 GB of heap at `N = 16`.+There are none left to leave unreduced, and it is now within noise of writing+the environment out — 2.25 s at `N = 64`.  A hand-written rule set need not+declare an environment at all.++**What this gives up.**  Left recursion was a type error, checked on every+compilation by `Acyclic`.  It is now checked once, by `PEG.Analysis`, when+`pegRules` or `pegGrammar` splices the grammar — which is where it was already+reported, with the rule and its cycle named, and which is the message you+actually saw.  What is no longer checked at all:++- A `Rules` chain assembled by hand from `RCons`, with no quasi-quoter+  involved.  A rule that begins with itself compiles and loops.+- Left recursion that closes *across* two `pegRules` blocks spliced together.+  A block is analysed open-world, since `RCons` lets two be combined, and+  `Acyclic` used to be the backstop.  Writing the grammar as a single+  `pegGrammar` closes the gap: it is closed-world.++`Star` no longer demands a non-nullable operand, for the same reason; a+nullable repetition is reported by `PEG.Analysis`, and by nothing at all if+the `Star` is built by hand.++**What this makes simpler.**  A combinator over expressions is now an ordinary+polymorphic function.  What had to be written++```haskell+lexeme :: PExp s env ty a -> PExp s env (SeqTy ty ('MkTy 'True '[])) a+```++is `PExp s env a -> PExp s env a`, and composes without the caller having to+get a nesting of type families right.  `examples/Patterns.hs` is where that+shows.++**And what now keeps the analysis honest.**  While the FIRST sets were also in+the types, `PEG.Analysis` could not be quietly wrong: `Grammar` demands+`Rules s env env`, so GHC recomputed everything and rejected an environment+that did not match.  It no longer does.  The `typed-peg-analysis` test-suite+therefore checks the analysis against a separate statement of what its results+mean — nullability as a least fixpoint, and a FIRST set as the transitive+closure of the one-step head relation — over every grammar in `examples/` and+over 400 generated ones, and asserts that the generated corpus keeps+containing both left-recursive and left-recursion-free grammars so the+agreement cannot go vacuous.++### Grammar checking at splice time++The environment a grammar declares is no longer something only GHC can+compute.  `PEG.Analysis` runs the same nullability and FIRST-set fixpoint in+ordinary Haskell, over the quasi-quoter's syntax tree.++*(Superseded above: the environment no longer states either, and+`PEG.Analysis` is the only thing that computes them.)*++Measurement first, because it redirected the work.  On a synthetic grammar of+`N` rules with `2N` non-terminal occurrences (`bench-compile/`, `ghc -fno-code+-freduction-depth=0`):++| N | environment written by hand | same, FIRST sets emptied | same, indices handed to GHC as literals |+|---|---|---|---|+| 16 | 0.59 s | 0.55 s | 0.52 s |+| 32 | 1.80 s | 1.09 s | 1.43 s |+| 48 | 6.39 s | 1.97 s | 5.19 s |+| 64 | 18.25 s | 3.75 s | 14.99 s |++Left to inference, the same grammar runs out of memory rather than time: at+`N = 16` GHC was past 24 GB of heap and still climbing.  It does derive+exactly the environment the examples write by hand — the entries it derives+are just full of unreduced type-family applications.++And, isolating the environment search alone — `N` entries, `2N` references:++| N | `Lookup` + `KnownMember` | witness, equality kept | witness, no equality |+|---|---|---|---|+| 32 | 0.55 s | 0.20 s | 0.18 s |+| 64 | 3.37 s | 0.86 s | 0.56 s |++Three quarters of it is the instance chain, and that quarter-to-three-quarters+split is the useful part: supplying the proof while keeping the `Lookup`+equality — so the reference still cannot name the wrong rule — collects most+of the win.  On the real library, on the grammar above, it is worth **2.6x**+at `N = 64`: 16.60 s becomes 6.27 s.++So the cost that remains after the 0.2 work is mostly **not** the FIRST-set+arithmetic: computing it in advance and handing GHC the answer is worth 1.2x.+It is the environment — searched once per occurrence of every non-terminal,+over entries whose size is dominated by the FIRST sets they carry.  Those are+two independent levers that compose: 2.6x for how a reference is resolved+(taken below) and 4.9x for what the entries carry (taken above — and the+1.2x turned out to be the whole of the arithmetic, so what the entries carry+cost nothing to compute and everything to have).  `PEG.Analysis` computes the+whole environment for the 64-rule grammar in 6 ms.++### Added++- `PEG.Analysis`: nullability, FIRST sets and well-formedness computed at+  splice time.  It was then the value-level twin of `PEG.TyLevel`, which was+  the specification; it is now the only implementation, and what checks it is+  the `typed-peg-analysis` test-suite.+- `pegRules` now reports left recursion, a nullable repetition and a duplicate+  rule **from the splice**, naming the rule and, for left recursion, the chain+  of head references that closes the cycle.  A block is analysed open-world,+  since `RCons` lets two blocks be combined, so an unknown name is treated as+  opaque rather than reported; `Acyclic` was then the backstop, and is now+  gone, so a cycle closing across two blocks is caught by nothing.  Write the+  grammar as one `pegGrammar` to close that gap.+- `PEG.QQ.Syntax`: the DSL's syntax tree and parser, split out of `PEG.QQ` so+  that the analysis and the translation can both consume it.+- **`pegGrammar`**, a quasi-quoter for a whole grammar.  In expression+  position it produces the `Grammar` value; in declaration position it also+  declares the environment and the signature, so that a grammar of `n` rules+  is `n` lines and nothing else:++  ```haskell+  [pegGrammar|+    %name  arith+    %start expr+    expr   :: Exp <- t:term ts:(o:[+-] u:term)* { foldl addOp t ts }+    ...+  |]+  ```++  Because it owns the whole grammar it knows each rule's position, so it emits+  `ntw` and the membership proof rather than `nt` and a search — **2.1x** on the+  64-rule grammar above.  It also knows that a name no rule defines is an+  error rather than a reference to somewhere else, so it says so at the+  splice.++  A rule's result type is the one thing the grammar does not determine, which+  is what the `:: T` annotations are for.  They are claims, not assertions:+  `Grammar` demands `Rules s env env`, so GHC checks each against what the+  rule body actually returns.  *(At the time this also meant GHC recomputed+  the FIRST sets and so could not be lied to about them; the entry above is+  what changed that.)*++  `examples/Arith.hs` and `examples/Layout.hs` are written this way now and+  declare no environment at all.  `pegRules` is unchanged and still the way to+  write a rule set that is only part of a grammar; `examples/Compat.hs` and+  `examples/Patterns.hs` keep using it.+- `PEG.Syntax.NTW` and `ntw`: a non-terminal reference that carries its own+  `Member` proof instead of having `KnownMember` search for it.  The `Lookup`+  equality is kept, so `ty` and `a` still come from the environment and a+  proof that names the wrong rule does not compile — this is not a weaker+  claim than `NT`, only a cheaper one.  Worth **2.6x** on a 64-rule grammar.+  A splice knows each rule's position and can write the proof down; a+  hand-written grammar has nothing to gain and should keep using `nt`.+- `bench-compile/`: a generator and a sweep script for the numbers above.+- A test-suite, `typed-peg-analysis`, that reads `examples/` and requires the+  computed environment of each grammar to equal the one written there.++### Compile time of large grammars  Checking a grammar was **exponential in the size of its FIRST sets**.  On a chain of `n` mutually referring rules, GHC needed 0.7 s at `n = 8`, 12 s at
README.md view
@@ -2,14 +2,15 @@  Type-safe PEG (Parsing Expression Grammar) parser combinators for Haskell. -Grammar non-terminals are indexed at the type level by their nullability and-FIRST sets, so left-recursive grammars are caught at compile time rather than-looping at runtime.+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.  ## Features -- Type-level FIRST-set and nullability tracking-- Compile-time left-recursion detection (type error)+- Non-terminal references checked at the type level+- Left recursion, a repetition that cannot consume input, an undefined+  non-terminal and a duplicate rule reported at the splice, naming the rule - Indentation-sensitive parsing (`PEG.Indent`) - Quasi-quoter for concrete grammar syntax (`PEG.QQ`) - Parses any `PEG.Stream`: `String`, strict/lazy `Text`, strict/lazy@@ -56,37 +57,99 @@  ```haskell import PEG+import PEG.QQ (pegGrammar) --- Define a grammar using the quasi-quoter--- See examples/Arith.hs for a complete arithmetic expression parser+data Exp = Lit Int | Add Exp Exp | Mul Exp Exp++[pegGrammar|+  %name  arith+  %start expr++  expr   :: Exp <- t:term ts:(o:[+] u:term)*     { foldl addOp t ts }+  term   :: Exp <- f:factor fs:(o:[*] g:factor)* { foldl addOp f fs }+  factor :: Exp <- n:number / '(' e:expr ')'+  number :: Exp <- ds:[0-9]+ { Lit (read (chunkToString ds)) }+|] ``` -## Grammar size+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"`. -The nullability and FIRST set of every rule are computed by GHC while it-type-checks the grammar, so a grammar's size shows up as compile time.  A-FIRST set is a type-level list of non-terminal names kept in **alphabetical-order**:+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. +`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.++## 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:+ ```haskell type CalcEnv =-  '[ '("expr" , 'EnvEntry ('MkTy 'False '["atom", "term", "unary"]) Expr)-   , '("term" , 'EnvEntry ('MkTy 'False '["atom", "unary"])         Expr)-   , '("unary", 'EnvEntry ('MkTy 'False '["atom"])                  Expr)-   , '("atom" , 'EnvEntry ('MkTy 'False '[])                        Expr)+  '[ '("expr" , 'EnvEntry Expr)+   , '("term" , 'EnvEntry Expr)+   , '("unary", 'EnvEntry Expr)+   , '("atom" , 'EnvEntry Expr)    ] ``` -The order is not cosmetic.  It gives a set exactly one spelling, which is what-lets the union of two FIRST sets be a single merge pass; listing one in some-other order is a type error naming the first position that disagrees.+Entries used to carry more: each rule's nullability and its FIRST set, the+non-terminals that can begin it.  That is what made left recursion a type+error — an `Acyclic` constraint checked that no rule was in its own FIRST set+— and it was also, measurably, the entire cost of compiling a large grammar.+A FIRST set grows with the grammar, so the environment was quadratic in the+number of rules, and each of the two reference constraints per rule was solved+against the whole of it.  Removing it took a 64-rule grammar from 15 s to 2 s,+and a 128-rule one from more than two minutes to 5 s.  `bench-compile/` has+the measurements. -That merge nests one type-family reduction per element of the result, so a-grammar with a FIRST set of more than about a hundred non-terminals hits GHC's-default reduction limit and reports `Reduction stack overflow`.  Add-`-freduction-depth=0` to `ghc-options` if you get there; it is a limit rather-than a slowdown, and a union of two 128-element sets takes about 0.3 s once it-is lifted.+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+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:++```+Arith.hs:8:13: error: [GHC-39584]+    • pegRules:+      left-recursive non-terminal: expr+        the cycle is expr -> term -> factor -> expr+        a PEG cannot backtrack into a committed choice, so this rule+        would not consume input before calling itself+```++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 rule's `:: T` annotation matches what its body returns | GHC | every compilation |+| left recursion, nullable repetition, duplicate rule | `PEG.Analysis` | at the splice |++The second half of that table is the trade.  A `Rules` chain assembled by hand+from `RCons`, without a quasi-quoter, is checked for reference errors only: a+rule that begins with itself compiles and loops.  And `pegRules` analyses its+block open-world, since two blocks can be combined, so left recursion that+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.++Since nothing recomputes what `PEG.Analysis` concludes, the+`typed-peg-analysis` test-suite checks it against a separate statement of what+nullability and a FIRST set mean, over the grammars in `examples/` and a few+hundred generated ones.  ## Patterns 
bench/Bench/Peg.hs view
@@ -62,14 +62,14 @@ readInt ds = Lit (read (chunkToString ds))  type ArithEnv =-  '[ '("expr"  , 'EnvEntry ('MkTy 'False '["factor", "number", "term"]) Exp)-   , '("term"  , 'EnvEntry ('MkTy 'False '["factor", "number"])         Exp)-   , '("factor", 'EnvEntry ('MkTy 'False '["number"])                   Exp)-   , '("number", 'EnvEntry ('MkTy 'False '[])                           Exp)+  '[ '("expr"  , 'EnvEntry Exp)+   , '("term"  , 'EnvEntry Exp)+   , '("factor", 'EnvEntry Exp)+   , '("number", 'EnvEntry Exp)    ]  {-# INLINABLE arith #-}-arith :: Stream s => Grammar s ArithEnv _ Exp+arith :: Stream s => Grammar s ArithEnv Exp arith =   Grammar     [pegRules|@@ -87,13 +87,13 @@ --------------------------------------------------------------------------------  type CsvEnv =-  '[ '("csv", 'EnvEntry ('MkTy 'False '["num", "row"]) [[Int]])-   , '("row", 'EnvEntry ('MkTy 'False '["num"])        [Int])-   , '("num", 'EnvEntry ('MkTy 'False '[])             Int)+  '[ '("csv", 'EnvEntry [[Int]])+   , '("row", 'EnvEntry [Int])+   , '("num", 'EnvEntry Int)    ]  {-# INLINABLE csv #-}-csv :: Stream s => Grammar s CsvEnv _ [[Int]]+csv :: Stream s => Grammar s CsvEnv [[Int]] csv =   Grammar     [pegRules|@@ -113,12 +113,12 @@ -- The environment is parameterised by the stream: @ident@ is a character -- class, so its result is a chunk of the input. type IdentEnv s =-  '[ '("idents", 'EnvEntry ('MkTy 'False '["ident"]) [s])-   , '("ident" , 'EnvEntry ('MkTy 'False '[])        s)+  '[ '("idents", 'EnvEntry [s])+   , '("ident" , 'EnvEntry s)    ]  {-# INLINABLE idents #-}-idents :: Stream s => Grammar s (IdentEnv s) _ [s]+idents :: Stream s => Grammar s (IdentEnv s) [s] idents =   Grammar     [pegRules|@@ -149,20 +149,20 @@ orEmpty (Just xs) = xs  type JsonEnv =-  '[ '("json"   , 'EnvEntry ('MkTy 'False '["array","number","object","strlit","value","ws"]) JValue)-   , '("value"  , 'EnvEntry ('MkTy 'False '["array","number","object","strlit"])              JValue)-   , '("object" , 'EnvEntry ('MkTy 'False '[])                                                JValue)-   , '("members", 'EnvEntry ('MkTy 'False '["pair","strlit"])                    [(String, JValue)])-   , '("pair"   , 'EnvEntry ('MkTy 'False '["strlit"])                             (String, JValue))-   , '("array"  , 'EnvEntry ('MkTy 'False '[])                                                JValue)-   , '("elems"  , 'EnvEntry ('MkTy 'False '["array","number","object","strlit","value"])    [JValue])-   , '("strlit" , 'EnvEntry ('MkTy 'False '[])                                                String)-   , '("number" , 'EnvEntry ('MkTy 'False '[])                                                JValue)-   , '("ws"     , 'EnvEntry ('MkTy 'True  '[])                                                    ())+  '[ '("json"   , 'EnvEntry JValue)+   , '("value"  , 'EnvEntry JValue)+   , '("object" , 'EnvEntry JValue)+   , '("members", 'EnvEntry [(String, JValue)])+   , '("pair"   , 'EnvEntry (String, JValue))+   , '("array"  , 'EnvEntry JValue)+   , '("elems"  , 'EnvEntry [JValue])+   , '("strlit" , 'EnvEntry String)+   , '("number" , 'EnvEntry JValue)+   , '("ws"     , 'EnvEntry ())    ]  {-# INLINABLE json #-}-json :: Stream s => Grammar s JsonEnv _ JValue+json :: Stream s => Grammar s JsonEnv JValue json =   Grammar     [pegRules|@@ -196,18 +196,18 @@  -- @(!'"' .)*@ is a compound repetition, so it still yields a @['Char']@ ... type QuotedNotEnv =-  '[ '("qs", 'EnvEntry ('MkTy 'False '["q"]) [String])-   , '("q" , 'EnvEntry ('MkTy 'False '[])    String)+  '[ '("qs", 'EnvEntry [String])+   , '("q" , 'EnvEntry String)    ]  -- ... whereas @[^"]*@ is a character class and yields a chunk. type QuotedClsEnv s =-  '[ '("qs", 'EnvEntry ('MkTy 'False '["q"]) [s])-   , '("q" , 'EnvEntry ('MkTy 'False '[])    s)+  '[ '("qs", 'EnvEntry [s])+   , '("q" , 'EnvEntry s)    ]  {-# INLINABLE quotedNot #-}-quotedNot :: Stream s => Grammar s QuotedNotEnv _ [String]+quotedNot :: Stream s => Grammar s 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 (QuotedClsEnv s) [s] quotedCls =   Grammar     [pegRules|
examples/Arith.hs view
@@ -1,9 +1,8 @@-{-# LANGUAGE DataKinds             #-}-{-# LANGUAGE QuasiQuotes           #-}-{-# LANGUAGE TypeApplications      #-}-{-# LANGUAGE TypeOperators         #-}-{-# LANGUAGE PartialTypeSignatures #-}-{-# OPTIONS_GHC -Wno-partial-type-signatures #-}+{-# LANGUAGE DataKinds        #-}+{-# LANGUAGE QuasiQuotes      #-}+{-# LANGUAGE TemplateHaskell  #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators    #-}  module Arith   ( Exp (..)@@ -14,7 +13,7 @@   ) where  import PEG-import PEG.QQ (pegRules)+import PEG.QQ (pegGrammar)  data Exp   = Lit Int@@ -51,29 +50,29 @@ addOp l ('/', r) = Div l r addOp _ (c  , _) = error ("addOp: unexpected operator " ++ show c) -type ArithEnv =-  '[ '("expr"  , 'EnvEntry ('MkTy 'False '["factor", "number", "term"]) Exp)-   , '("term"  , 'EnvEntry ('MkTy 'False '["factor", "number"])         Exp)-   , '("factor", 'EnvEntry ('MkTy 'False '["number"])                   Exp)-   , '("number", 'EnvEntry ('MkTy 'False '[])                           Exp)-   ]---- | Polymorphic in the stream, so the same grammar can be run over 'String',--- 'Data.Text.Text' and 'Data.ByteString.ByteString'.  Note the cost: this is--- a function of a 'Stream' dictionary rather than a constant, so the compiled--- parser is not shared between calls.  Bind a monomorphic parser+-- | 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 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.+--+-- 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+-- shared between calls.  Bind a monomorphic parser -- (@arithString = parse arith :: String -> Result String Exp@) where that -- matters.-arith :: Stream s => Grammar s ArithEnv _ Exp-arith =-  Grammar-    [pegRules|-       expr   <- t:term ts:(o:[+-] u:term)* { foldl addOp t ts }-       term   <- f:factor fs:(o:[*/] g:factor)*-                   { foldl (\acc (op, r) -> addOp acc (op, r)) f fs }-       factor <- n:number-               / '(' e:expr ')'-               / '-' f:factor { Neg f }-       number <- ds:[0-9]+ { Lit (read (chunkToString ds) :: Int) }-    |]-    (nt @"expr")+[pegGrammar|+  %name  arith+  %start expr++  expr   :: Exp <- t:term ts:(o:[+-] u:term)* { foldl addOp t ts }+  term   :: Exp <- f:factor fs:(o:[*/] g:factor)*+                     { foldl (\acc (op, r) -> addOp acc (op, r)) f fs }+  factor :: Exp <- n:number+                 / '(' e:expr ')'+                 / '-' f:factor { Neg f }+  number :: Exp <- ds:[0-9]+ { Lit (read (chunkToString ds) :: Int) }+|]
examples/Compat.hs view
@@ -1,9 +1,7 @@ {-# LANGUAGE OverloadedStrings      #-}-{-# LANGUAGE PartialTypeSignatures  #-} {-# LANGUAGE QuasiQuotes            #-} {-# LANGUAGE RankNTypes             #-} {-# LANGUAGE ScopedTypeVariables    #-}-{-# OPTIONS_GHC -Wno-partial-type-signatures #-}  -- | A differential test: it renders the complete 'Result' (value, consumed -- prefix and remaining suffix) for a fixed battery of inputs.@@ -81,11 +79,11 @@ -- reaches them.  @!c*@ can never succeed, because the star matches the empty -- run. type SpanEnv s =-  '[ '("digits", 'EnvEntry ('MkTy 'True  '[]) s)-   , '("digits1", 'EnvEntry ('MkTy 'False '[]) s)+  '[ '("digits", 'EnvEntry s)+   , '("digits1", 'EnvEntry s)    ] -spanG :: Stream s => Grammar s (SpanEnv s) _ (s, s)+spanG :: Stream s => Grammar s (SpanEnv s) (s, s) spanG =   Grammar     [pegRules|@@ -96,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 '[] Char notSpan1G = Grammar RNil [pegExpr| !'x'+ c:. |] -notSpanG :: Stream s => Grammar s '[] _ Char+notSpanG :: Stream s => Grammar s '[] Char notSpanG = Grammar RNil [pegExpr| !'x'* c:. |]  spanCases :: [String]
examples/Layout.hs view
@@ -1,9 +1,8 @@-{-# LANGUAGE DataKinds             #-}-{-# LANGUAGE QuasiQuotes           #-}-{-# LANGUAGE TypeApplications      #-}-{-# LANGUAGE TypeOperators         #-}-{-# LANGUAGE PartialTypeSignatures #-}-{-# OPTIONS_GHC -Wno-partial-type-signatures #-}+{-# LANGUAGE DataKinds        #-}+{-# LANGUAGE QuasiQuotes      #-}+{-# LANGUAGE TemplateHaskell  #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators    #-}  module Layout   ( DoStmt (..)@@ -13,43 +12,38 @@   ) where  import PEG-import PEG.QQ (pegExpr, pegRules)+import PEG.QQ (pegGrammar)  data DoStmt   = Atom   String   | Nested [DoStmt]   deriving (Eq, Show) --- | The environment is parameterised by the stream, because @name@ is a--- character class and so produces a chunk of the input rather than a--- 'String'.  Any rule whose result is a chunk pushes @s@ into the--- environment this way.-type DoEnv s =-  '[ '("doexp" , 'EnvEntry ('MkTy 'False '[])                               [DoStmt])-   , '("istmts", 'EnvEntry ('MkTy 'False '["doexp", "name", "stmt", "ws"]) [DoStmt])-   , '("stmts" , 'EnvEntry ('MkTy 'False '["ws"])                          [DoStmt])-   , '("stmt"  , 'EnvEntry ('MkTy 'False '["doexp", "name"])               DoStmt)-   , '("name"  , 'EnvEntry ('MkTy 'False '[])                              s)-   , '("ws"    , 'EnvEntry ('MkTy 'True  '[])                              ())-   ]+-- | @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.+--+-- @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+-- start expression is different — @%start ws d:doexp ws !.@ returns what its+-- one labelled item returns.+[pegGrammar|+  %name  doExp+  %env   DoEnv+  %start ws d:doexp ws !. -doExp :: Stream s => Grammar s (DoEnv s) _ [DoStmt]-doExp =-  Grammar-    [pegRules|-       doexp  <- "do" b:(i:istmts / j:stmts)+  doexp  :: [DoStmt] <- "do" b:(i:istmts / j:stmts) -       istmts <- ss:(ws st:|s:stmt|)+^>+  istmts :: [DoStmt] <- ss:(ws st:|s:stmt|)+^> -       stmts  <- r:(ws '{' ws s:stmt ss:(ws ';' ws t:stmt)* ws '}' { s : ss })^~+  stmts  :: [DoStmt] <- r:(ws '{' ws s:stmt ss:(ws ';' ws t:stmt)* ws '}' { s : ss })^~ -       stmt   <- d:doexp { Nested d } / n:name { Atom (chunkToString n) }+  stmt   :: DoStmt   <- d:doexp { Nested d } / n:name { Atom (chunkToString n) } -       name   <- cs:[a-z]++  name   :: s        <- cs:[a-z]+ -       ws     <- [ \t\r\n]*_~-    |]-    [pegExpr| ws d:doexp ws !. |]+  ws     :: ()       <- [ \t\r\n]*_~+|]  layoutOpts :: Opts layoutOpts = defaultOpts { optTokenMode = relD geR }
examples/Patterns.hs view
@@ -3,8 +3,6 @@ {-# LANGUAGE QuasiQuotes           #-} {-# LANGUAGE TypeApplications      #-} {-# LANGUAGE TypeOperators         #-}-{-# LANGUAGE PartialTypeSignatures #-}-{-# OPTIONS_GHC -Wno-partial-type-signatures #-}  -- | Worked examples for @peg-patterns.md@. --@@ -31,21 +29,30 @@  -- | Zero or more layout characters.  A character class, so this compiles to a -- single 'Span' node and returns a chunk of the input.-ws :: PExp s env ('MkTy 'True '[]) s+ws :: PExp s env s ws = spanOf (fromRanges [(' ', ' '), ('\t', '\t'), ('\r', '\r'), ('\n', '\n')])  -- | Run @p@, then consume /trailing/ whitespace only.-lexeme :: PExp s env ty a -> PExp s env (SeqTy ty ('MkTy 'True '[])) a+--+-- A combinator over expressions is an ordinary polymorphic function.  It did+-- not use to be: when a 'PExp' carried its nullability and FIRST set in a+-- fourth index, this had to be written+--+-- @+-- lexeme :: PExp s env ty a -> PExp s env (SeqTy ty ('MkTy 'True '[])) a+-- @+--+-- and every combinator built on it had to restate the nesting exactly.  See+-- "PEG.Type" for where those indices went.+lexeme :: PExp s env a -> PExp s env a lexeme p = (\x _ -> x) <$>. p <*>. ws  -- | End of input: nothing can follow.-eof :: PExp s env ('MkTy 'True '[]) ()+eof :: PExp s env () eof = Not AnyChar  -- | Leading whitespace, then @p@, then end of input.-fully :: PExp s env ty a-      -> PExp s env (SeqTy ('MkTy 'True '[])-                           (SeqTy ty ('MkTy 'True '[]))) a+fully :: PExp s env a -> PExp s env a fully p = (\_ x _ -> x) <$>. ws <*>. p <*>. eof  --------------------------------------------------------------------------------@@ -60,7 +67,7 @@ -- The negative lookahead is the whole pattern: @keyword "negate"@ fails on -- @negatex@ because an identifier character follows.  In a backtracking -- combinator library this needs @try@; in a PEG it is just @!@.-keyword :: String -> PExp s env ('MkTy 'False '[]) ()+keyword :: String -> PExp s env () keyword k = (\_ _ -> ()) <$>. stringNE k <*>. Not (sat identCont)  --------------------------------------------------------------------------------@@ -121,18 +128,18 @@ --------------------------------------------------------------------------------  type CalcEnv s =-  '[ '("expr" , 'EnvEntry ('MkTy 'False '["atom", "term", "unary"]) Expr)-   , '("term" , 'EnvEntry ('MkTy 'False '["atom", "unary"])         Expr)-   , '("unary", 'EnvEntry ('MkTy 'False '["atom"])                  Expr)-   , '("atom" , 'EnvEntry ('MkTy 'False '[])                        Expr)+  '[ '("expr" , 'EnvEntry Expr)+   , '("term" , 'EnvEntry Expr)+   , '("unary", 'EnvEntry Expr)+   , '("atom" , 'EnvEntry Expr)    ]  -- | The classic expression language. -- -- Note what is /not/ here: no @try@, no left recursion, and no rule that can--- loop.  @expr <- expr '+' term@ would be rejected by 'PEG.Grammar.Acyclic'--- at compile time with a type error naming @expr@.-calc :: Stream s => Grammar s (CalcEnv s) _ Expr+-- 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 =   Grammar     [pegRules|@@ -148,9 +155,9 @@  -- | The same pattern inside a quasi-quoted grammar: a string literal followed -- by a negative lookahead on the identifier-continuation class.-type KwEnv = '[ '("kw", 'EnvEntry ('MkTy 'False '[]) String) ]+type KwEnv = '[ '("kw", 'EnvEntry String) ] -kwG :: Stream s => Grammar s KwEnv _ String+kwG :: Stream s => Grammar s KwEnv String kwG = Grammar [pegRules| kw <- k:"negate" ![a-zA-Z0-9_]  { k } |] (nt @"kw")  --------------------------------------------------------------------------------@@ -163,9 +170,9 @@ --------------------------------------------------------------------------------  type OpEnv =-  '[ '("op", 'EnvEntry ('MkTy 'False '[]) (Expr -> Expr -> Expr)) ]+  '[ '("op", 'EnvEntry (Expr -> Expr -> Expr)) ] -addOp :: Stream s => Grammar s OpEnv _ (Expr -> Expr -> Expr)+addOp :: Stream s => Grammar s OpEnv (Expr -> Expr -> Expr) addOp = Grammar [pegRules| op <- '+' { Add } / '-' { Sub } |] (nt @"op")  --------------------------------------------------------------------------------@@ -173,12 +180,12 @@ --------------------------------------------------------------------------------  type ProgEnv s =-  '[ '("prog" , 'EnvEntry ('MkTy 'False '["asgn"]) [Asgn])-   , '("asgn" , 'EnvEntry ('MkTy 'False '[])       Asgn)-   , '("expr" , 'EnvEntry ('MkTy 'False '["atom", "term", "unary"]) Expr)-   , '("term" , 'EnvEntry ('MkTy 'False '["atom", "unary"])         Expr)-   , '("unary", 'EnvEntry ('MkTy 'False '["atom"])                  Expr)-   , '("atom" , 'EnvEntry ('MkTy 'False '[])                        Expr)+  '[ '("prog" , 'EnvEntry [Asgn])+   , '("asgn" , 'EnvEntry Asgn)+   , '("expr" , 'EnvEntry Expr)+   , '("term" , 'EnvEntry Expr)+   , '("unary", 'EnvEntry Expr)+   , '("atom" , 'EnvEntry Expr)    ]  -- | @a := 1; b := a * 2@@@ -187,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 (ProgEnv s) [Asgn] prog =   Grammar     [pegRules|
peg-patterns.md view
@@ -11,11 +11,13 @@ - **Ordered choice is committed.** Once an alternative succeeds, a PEG never   reconsiders it. There is no `try`, because there is nothing to undo — but the   order in which you write alternatives becomes part of the specification.-- **Left recursion is a type error**, not a discipline to remember. The-  `Acyclic` constraint is checked when you construct a `Grammar`.-- **The grammar's shape is written down in a type.** The `Env` records every-  rule's nullability, FIRST set and result type. Several of the paper's-  patterns become things the compiler enforces rather than things you adopt.+- **Left recursion is rejected when you write the grammar**, not a discipline+  to remember. `PEG.Analysis` runs inside the quasi-quoter and reports it,+  naming the rule and the cycle.+- **The grammar's shape is written down in a type.** The `Env` records what+  every rule returns, so a reference to a rule that does not exist, or at the+  wrong type, is a type error. Several of the paper's patterns become things+  the compiler enforces rather than things you adopt.  Every code fragment below is compiled: it lives in [`examples/Patterns.hs`](examples/Patterns.hs) and runs as part of@@ -55,20 +57,23 @@  and observing that it loops. Section 2 is then about the rewrite that fixes it. -In typed-peg you cannot write it in the first place. Each rule's type carries-its FIRST set, and `Grammar` demands `Acyclic env`:+In typed-peg you cannot write it in the first place. The quasi-quoter+computes each rule's FIRST set as it splices the grammar, and rejects one that+contains its own rule:  ``` expr <- e:expr '+' t:term { Add e t } ```  ```-Left-recursive non-terminal: "expr"-Its head set already contains itself: ["expr", "term"]-Violates the acyclicity condition i `notElem` Gamma(i).F.+    • pegRules:+      left-recursive non-terminal: expr+        the cycle is expr -> expr+        a PEG cannot backtrack into a committed choice, so this rule+        would not consume input before calling itself ``` -reported at the `Grammar` constructor, before anything runs.+reported at the quasi-quoter, before anything runs.  **Pattern.** Do not treat left-recursion removal as a step you perform. Write the grammar; if it compiles, no rule can loop on its own head. The rewrite@@ -117,10 +122,10 @@  ```haskell type CalcEnv s =-  '[ '("expr" , 'EnvEntry ('MkTy 'False '["atom", "term", "unary"]) Expr)-   , '("term" , 'EnvEntry ('MkTy 'False '["atom", "unary"])         Expr)-   , '("unary", 'EnvEntry ('MkTy 'False '["atom"])                  Expr)-   , '("atom" , 'EnvEntry ('MkTy 'False '[])                        Expr)+  '[ '("expr" , 'EnvEntry Expr)+   , '("term" , 'EnvEntry Expr)+   , '("unary", 'EnvEntry Expr)+   , '("atom" , 'EnvEntry Expr)    ] ``` @@ -140,17 +145,21 @@ makes a misplaced operator a type error, exactly as in the paper — at the cost of an AST with one constructor per layer. -Note the FIRST set columns. They are not decoration: `'["atom", "term", "unary"]` says that entering `expr` can immediately enter any of those, and it-is what the acyclicity check consumes. Getting them wrong is a compile error,-so they double as a checked comment.+An entry used to carry the rule's nullability and FIRST set as well —+`'("expr", 'EnvEntry ('MkTy 'False '["atom", "term", "unary"]) Expr)` — which+is what the acyclicity check consumed. It was also the whole cost of compiling+a large grammar, because a FIRST set grows with the grammar and the+environment is solved against once per reference; see `bench-compile/`. The+sets are now computed by `PEG.Analysis` at the splice instead, so what is left+to write down is the part only you know: what the rule returns. -A FIRST set is written in **alphabetical order**, and with no repeats. The-order is not a matter of taste: it is what makes a set have exactly one-spelling, which in turn lets the compiler take the union of two FIRST sets in-one merge pass instead of re-scanning one of them for every element of the-other. Write `'["atom", "term", "unary"]`, not `'["term", "unary", "atom"]`;-the latter is a type error naming the first position that disagrees.+Better still, do not write it down at all. `pegGrammar` generates the+environment from the same `:: T` annotations: +```+expr :: Expr <- t:term ts:(o:[+-] u:term)* { chainl t ts }+```+ ### 1.4 Precedence tables: absent, but not impossible  *(Willis & Wu, Pattern 1c: Precedence Tables.)*@@ -203,21 +212,24 @@ position reporting and makes it ambiguous who is responsible for a given space.  ```haskell-ws :: PExp s env ('MkTy 'True '[]) s+ws :: PExp s env s ws = spanOf (fromRanges [(' ', ' '), ('\t', '\t'), ('\r', '\r'), ('\n', '\n')]) -lexeme :: PExp s env ty a -> PExp s env (SeqTy ty ('MkTy 'True '[])) a+lexeme :: PExp s env a -> PExp s env a lexeme p = (\x _ -> x) <$>. p <*>. ws -eof :: PExp s env ('MkTy 'True '[]) ()+eof :: PExp s env () eof = Not AnyChar -fully :: PExp s env ty a-      -> PExp s env (SeqTy ('MkTy 'True '[])-                           (SeqTy ty ('MkTy 'True '[]))) a+fully :: PExp s env a -> PExp s env a fully p = (\_ x _ -> x) <$>. ws <*>. p <*>. eof ``` +These are ordinary polymorphic functions. They did not use to be: while a+`PExp` carried its nullability and FIRST set in a fourth index, `lexeme` had+to be written `PExp s env ty a -> PExp s env (SeqTy ty ('MkTy 'True '[])) a`+and everything built on it had to restate the nesting exactly.+ ``` "12"     => OK "12" "  12  " => OK "12"@@ -272,7 +284,7 @@ In a PEG it is just `!`:  ```haskell-keyword :: String -> PExp s env ('MkTy 'False '[]) ()+keyword :: String -> PExp s env () keyword k = (\_ _ -> ()) <$>. stringNE k <*>. Not (sat identCont) ``` @@ -358,9 +370,9 @@ just a rule whose result type is a function:  ```haskell-type OpEnv = '[ '("op", 'EnvEntry ('MkTy 'False '[]) (Expr -> Expr -> Expr)) ]+type OpEnv = '[ '("op", 'EnvEntry (Expr -> Expr -> Expr)) ] -addOp :: Stream s => Grammar s OpEnv _ (Expr -> Expr -> Expr)+addOp :: Stream s => Grammar s OpEnv (Expr -> Expr -> Expr) addOp = Grammar [pegRules| op <- '+' { Add } / '-' { Sub } |] (nt @"op") ``` @@ -471,7 +483,7 @@ ``` Couldn't match expected type 's' with actual type '[Char]'   's' is a rigid type variable bound by the inferred type of-    identG :: Stream s => Grammar s (IdEnv s) (MkTy False '["ident"]) s+    identG :: Stream s => Grammar s (IdEnv s) s ```  So the idiom quietly ties a grammar to one stream. The fix is also faster,@@ -494,14 +506,16 @@  ```haskell type CalcEnv s =-  '[ '("expr" , 'EnvEntry ('MkTy 'False '["atom", "term", "unary"]) Expr)+  '[ '("expr" , 'EnvEntry Expr)    , ...    ] ``` -Each entry states three things: whether the rule can match the empty string,-which non-terminals it can enter first, and what it produces. All three are-verified against the rule bodies.+Each entry states what the rule produces, and that is verified against the+rule body. Entries used to state two things more — whether the rule can match+the empty string, and which non-terminals it can enter first — which is what+made left recursion a type error and what made a large grammar slow to+compile; both now happen at the splice instead.  **Pattern.** Write the `Env` before the rules, as you would write a signature before a function. When a rule's FIRST set surprises you, that is usually the
src/PEG.hs view
@@ -1,21 +1,30 @@ -- | Type-safe PEG (Parsing Expression Grammar) parser combinators. ----- Grammar non-terminals are indexed at the type level by their nullability and--- FIRST sets. Left-recursive grammars are rejected at compile time via a--- 'GHC.TypeLits.TypeError'.+-- Grammar non-terminals are checked at the type level against an environment+-- that binds each rule's name to the type it returns, so a reference to a+-- rule that does not exist, or a use of one at the wrong type, is a type+-- error.  Left recursion, a repetition that cannot consume input, an+-- undefined non-terminal and a duplicate rule are rejected by the+-- quasi-quoter when the grammar is spliced; see "PEG.Grammar" for exactly+-- what is checked where. -- -- == Quick start -- -- @ -- import PEG--- import PEG.QQ (pegRules)+-- import PEG.QQ (pegGrammar) -- @ ----- 1. Declare the grammar environment as a type-level list of @(name, entry)@---    pairs (see 'PEG.Type.Env').--- 2. Build a 'Grammar' using 'pegRules' (quasi-quoter) or the combinators in---    "PEG.Syntax".--- 3. Run the grammar on a 'String' with 'parse' or 'parseWith'.+-- 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.+-- 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. -- -- See the @examples/@ directory for complete working grammars. module PEG
+ src/PEG/Analysis.hs view
@@ -0,0 +1,362 @@+-- | Nullability, FIRST sets and well-formedness, computed in Haskell.+--+-- This module is the value-level twin of "PEG.TyLevel" and "PEG.Grammar":+-- it computes, from the grammar DSL's syntax tree, exactly the environment+-- that GHC would otherwise derive by reducing 'PEG.Syntax.SeqTy',+-- 'PEG.Syntax.ChoiceTy' and 'PEG.TyLevel.Union' while type-checking a+-- 'PEG.Grammar.Rules' value.+--+-- == Why it moved here+--+-- It used to be a type-level computation, and that is what made left+-- recursion a type error.  It was billed to every compilation of every module+-- that mentioned the grammar, and its cost grew sharply with the grammar's+-- size: every entry of the environment carried a FIRST set, so the+-- environment was quadratic in the number of rules, and each of the two+-- reference constraints per rule had to be solved against it.  A 64-rule+-- grammar cost GHC 15 s.  The same fixpoint runs here, at splice time and+-- once, in 6 ms for that grammar and 286 ms for one of 256 rules.  See+-- @bench-compile/@ for the measurements and "PEG.Type" for the trade.+--+-- == This module is now load-bearing+--+-- While the FIRST sets were also in the types, this module could be wrong+-- without being dangerous: GHC recomputed everything and rejected a grammar+-- whose environment did not match.  It no longer does.  A left-recursive+-- grammar that this module accepts is a parser that loops.+--+-- What replaces the type checker is @tests/typed-peg-analysis@, which checks+-- the results here against a straightforward statement of what they mean —+-- nullability as a least fixpoint, and a FIRST set as the transitive closure+-- of the one-step head relation — over the grammars in @examples/@ and over a+-- few hundred generated ones.+--+-- == The fixpoint+--+-- Nullability and FIRST are both computed as the least solution of the+-- equations the type families state.  For a grammar without left recursion+-- that solution is the only one, which is why GHC can find it by unification+-- alone; for a left-recursive grammar the least solution is the one that puts+-- a non-terminal in its own FIRST set, which is precisely what+-- 'PEG.Grammar.Acyclic' rejects.+module PEG.Analysis+  ( Ty (..)+  , RuleEnv+  , Diagnostic (..)+  , World (..)+  , analyse+  , analyseWith+  , exprTy+  , seqTy+  , choiceTy+  , insertSym+  , unionSym+  , renderEnv+  , renderDiagnostic+  , spannable+  ) where++import Data.List (foldl1', nub)+import Data.Maybe (fromMaybe)++import PEG.QQ.Syntax (Def (..), Item (..), PExpr (..))++-- | The value-level image of 'PEG.Type.Ty': a nullability flag and a FIRST+-- set of non-terminal names.+--+-- The FIRST set is kept strictly sorted by 'compare', which agrees with+-- 'GHC.TypeLits.CmpSymbol' on the identifiers the DSL admits.  Sortedness is+-- what makes the set canonical, so that a generated environment is+-- /syntactically/ the type GHC computes rather than merely an equivalent one.+data Ty = Ty+  { tyNullable :: !Bool+  , tyFirst    :: ![String]+  } deriving (Eq, Show)++-- | A grammar environment in definition order: the value-level image of+-- 'PEG.Type.Env', minus the result types, which only the type checker knows.+type RuleEnv = [(String, Ty)]++-- | Something that makes the grammar ill-formed.+--+-- Each of these used to be a type error — or, in the case of 'NullableStar',+-- a type error whose message mentioned neither the rule nor the repetition+-- that caused it.  Reporting them here means naming the rule, and is now the+-- only place any of them is reported.+data Diagnostic+  = -- | A non-terminal is in its own FIRST set, with the chain of head+    -- references that puts it there.+    LeftRecursive String [String]+  | -- | @e*@ or @e+@ where @e@ can match the empty string: the repetition+    -- would not consume input and the parser would not terminate.+    NullableStar String+  | -- | A rule body references a name that no rule defines; the second field+    -- lists the names that are defined.+    UndefinedNT String [String]+  | -- | Two rules with the same name.+    DuplicateRule String+  deriving (Eq, Show)++--------------------------------------------------------------------------------+-- Sorted sets, mirroring PEG.TyLevel+--------------------------------------------------------------------------------++-- | The image of 'PEG.TyLevel.ConsIfAbsent'.+insertSym :: String -> [String] -> [String]+insertSym x [] = [x]+insertSym x (y:ys) = case compare x y of+  LT -> x : y : ys+  EQ -> y : ys+  GT -> y : insertSym x ys++-- | The image of 'PEG.TyLevel.Union': a single merge pass over two sorted+-- sets.+unionSym :: [String] -> [String] -> [String]+unionSym [] ys = ys+unionSym xs [] = xs+unionSym (x:xs) (y:ys) = case compare x y of+  LT -> x : unionSym xs (y:ys)+  EQ -> x : unionSym xs ys+  GT -> y : unionSym (x:xs) ys++-- | The image of 'PEG.Syntax.SeqTy'.+seqTy :: Ty -> Ty -> Ty+seqTy t1 t2 =+  Ty (tyNullable t1 && tyNullable t2)+     (unionSym (tyFirst t1) (if tyNullable t1 then tyFirst t2 else []))++-- | The image of 'PEG.Syntax.ChoiceTy'.+choiceTy :: Ty -> Ty -> Ty+choiceTy t1 t2 =+  Ty (tyNullable t1 || tyNullable t2)+     (unionSym (tyFirst t1) (tyFirst t2))++nullTy, termTy :: Ty+nullTy = Ty True  []+termTy = Ty False []++--------------------------------------------------------------------------------+-- The type of one expression+--------------------------------------------------------------------------------++-- | The 'Ty' of a DSL expression, given the 'Ty' of every non-terminal it may+-- reference.+--+-- This has to follow @PEG.QQ.translateExpr@ case for case, including its+-- optimisations: a repetition of a bare class, character or dot compiles to+-- 'PEG.Syntax.Span' or 'PEG.Syntax.Span1' rather than to+-- 'PEG.Syntax.Star', and those two have different FIRST sets from the generic+-- form.  A case that disagrees with the translation produces an environment+-- GHC will reject.+exprTy :: (String -> Ty) -> PExpr -> Ty+exprTy look = go+  where+    go (EChar _)        = termTy+    go EDot             = termTy+    go (EClass _ _)     = termTy+    go (EString s)+      | null s          = nullTy          -- pureP ""+      | otherwise       = termTy+    -- NTGo: the reference adds its own name to the rule's FIRST set.+    go (ENT n)          = let t = look n+                          in Ty (tyNullable t) (insertSym n (tyFirst t))+    -- Both lookaheads are 'Not' at bottom, which is nullable and keeps the+    -- FIRST set of its operand.  @&e@ is @Not (Not e)@.+    go (EAnd e)         = Ty True (tyFirst (go e))+    go (ENot e)         = Ty True (tyFirst (go e))+    go (EOpt e)         = choiceTy (go e) nullTy+    go (EStar e)+      | spannable e     = nullTy          -- spanOf+      | otherwise       = Ty True (tyFirst (go e))+    go (EPlus e)+      | spannable e     = termTy          -- spanOf1+      | otherwise       = let t = go e in seqTy t (Ty True (tyFirst t))+    go (EIndent _ e)    = go e+    go (EPos _ e)       = go e+    go (EAlign e)       = go e+    go (EChoice es)     = foldl1' choiceTy (map go es)+    go (ESeq [] _)      = nullTy          -- pureP+    go (ESeq items _)   = foldl1' seqTy [ go e | Item _ e <- items ]++-- | Does a repetition of this expression compile to a 'PEG.Syntax.Span'?+spannable :: PExpr -> Bool+spannable (EClass _ _) = True+spannable (EChar _)    = True+spannable EDot         = True+spannable _            = False++--------------------------------------------------------------------------------+-- The grammar+--------------------------------------------------------------------------------++-- | Is this the whole grammar, or part of one?+--+-- 'PEG.Grammar.RCons' is exported, so two quasi-quoted blocks can be spliced+-- into one rule set and a rule in the first may reference a rule in the+-- second.  A block analysed 'Open' therefore treats an unknown name as+-- opaque — non-nullable, with an empty FIRST set — instead of reporting it.+--+-- Under-approximating a FIRST set loses a 'LeftRecursive' or a+-- 'NullableStar'; over-approximating would reject a grammar that is fine.+-- The second is the worse failure, so an unknown name is treated as opaque —+-- but nothing catches what that loses, since the type checker no longer+-- computes FIRST sets of its own.  Left recursion that closes across two+-- blocks spliced together is reported by nobody; a grammar written as a+-- single 'PEG.QQ.pegGrammar' is 'Closed' and has no such gap.  The+-- environment returned for an 'Open' block is, for the same reason, not the+-- grammar's environment: only the diagnostics are meaningful.+data World = Closed | Open+  deriving (Eq, Show)++-- | Compute the environment of a complete set of rules, or report why it has+-- none.+--+-- All diagnostics of a kind are reported together, so a grammar with three+-- undefined non-terminals names all three rather than one per recompilation.+analyse :: [Def] -> Either [Diagnostic] RuleEnv+analyse = analyseWith Closed++-- | 'analyse', over a whole grammar or a fragment of one.+analyseWith :: World -> [Def] -> Either [Diagnostic] RuleEnv+analyseWith world defs+  | not (null dups)      = Left dups+  | not (null undefs)    = Left undefs+  | not (null illFormed) = Left illFormed+  | not (null leftRecs)  = Left leftRecs+  | otherwise            = Right env+  where+    names = [ n | Def n _ _ <- defs ]++    dups = [ DuplicateRule n+           | n <- nub names, length (filter (== n) names) > 1 ]++    undefs = case world of+      Open   -> []+      Closed -> [ UndefinedNT n names+                | n <- nub (concatMap (refs . body) defs), n `notElem` names ]+      where body (Def _ _ e) = e++    -- 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 ]+      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 ]++    at m n = fromMaybe (Ty False []) (lookup n m)++    -- 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 ]++    -- 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 ]+      where+        dedupe _ [] = []+        dedupe seen (d@(LeftRecursive _ path) : rest)+          | key `elem` seen = dedupe seen rest+          | otherwise       = d : dedupe (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.+    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++    -- 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))++    bodyOf n = case [ e | Def m _ e <- defs, m == n ] of+                 (e:_) -> e+                 []    -> ESeq [] Nothing++    cycleFrom n = go [n] n+      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+                     []    -> []++-- | Every non-terminal a body references, at any position.+refs :: PExpr -> [String]+refs (ENT n)       = [n]+refs (EAnd e)      = refs e+refs (ENot e)      = refs e+refs (EOpt e)      = refs e+refs (EStar e)     = refs e+refs (EPlus e)     = refs e+refs (EIndent _ e) = refs e+refs (EPos _ e)    = refs e+refs (EAlign e)    = refs e+refs (EChoice es)  = concatMap refs es+refs (ESeq its _)  = concat [ refs e | Item _ e <- its ]+refs _             = []++-- | Does the expression contain a repetition whose operand is nullable?+hasNullableRep :: (String -> Ty) -> PExpr -> Bool+hasNullableRep look = go+  where+    go (EStar e)     = (not (spannable e) && tyNullable (exprTy look e)) || go e+    go (EPlus e)     = (not (spannable e) && tyNullable (exprTy look e)) || go e+    go (EAnd e)      = go e+    go (ENot e)      = go e+    go (EOpt e)      = go e+    go (EIndent _ e) = go e+    go (EPos _ e)    = go e+    go (EAlign e)    = go e+    go (EChoice es)  = any go es+    go (ESeq its _)  = or [ go e | Item _ e <- its ]+    go _             = False++--------------------------------------------------------------------------------+-- Rendering+--------------------------------------------------------------------------------++-- | Render an environment as the source of a 'PEG.Type.Env' type, given a+-- result type for each rule.+--+-- Used to tell a user what to write while the environment still has to be+-- written by hand.  An entry no longer carries a FIRST set, so the analysis+-- contributes only the rule names and their order; what used to be the+-- interesting half of this function is now something no one has to write+-- down.+renderEnv :: (String -> String) -> RuleEnv -> String+renderEnv resultOf entries = unlines (zipWith line prefixes entries) ++ "   ]"+  where+    prefixes = "  '[ " : repeat "   , "+    line p (n, _) =+      p ++ "'(" ++ show n ++ ", 'EnvEntry " ++ resultOf n ++ ")"++-- | A one-paragraph explanation of a 'Diagnostic', in the shape the+-- quasi-quoter reports it.+renderDiagnostic :: Diagnostic -> String+renderDiagnostic (LeftRecursive n path) =+  "left-recursive non-terminal: " ++ n+    ++ (if null path then "" else "\n  the cycle is " ++ arrows path)+    ++ "\n  a PEG cannot backtrack into a committed choice, so this rule\n"+    ++ "  would not consume input before calling itself"+  where arrows = foldr1 (\a b -> a ++ " -> " ++ b)+renderDiagnostic (NullableStar n) =+  "in rule " ++ n ++ ": a repetition whose operand can match the empty\n"+    ++ "  string; it would not consume input and the parse would not terminate"+renderDiagnostic (UndefinedNT n defined) =+  "undefined non-terminal: " ++ n+    ++ "\n  the grammar defines " ++ unwords defined+renderDiagnostic (DuplicateRule n) =+  "the rule " ++ n ++ " is defined twice"
src/PEG/Grammar.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE ConstraintKinds      #-} {-# LANGUAGE DataKinds            #-} {-# LANGUAGE FlexibleContexts     #-} {-# LANGUAGE GADTs                #-}@@ -7,24 +6,50 @@ {-# LANGUAGE TypeOperators        #-} {-# LANGUAGE UndecidableInstances #-} --- | Grammar type and the acyclicity constraint.+-- | Grammar and rule-set types. -- -- A 'Grammar' bundles a set of named rules ('Rules') and a start expression.--- The 'Acyclic' constraint is checked at the definition site of every--- 'Grammar' value: if any non-terminal is left-recursive (its own name appears--- in its own FIRST set), GHC emits a 'GHC.TypeLits.TypeError' naming the--- offending non-terminal.+--+-- == Where left recursion is caught+--+-- In the quasi-quoters, and only there.  'PEG.QQ.pegGrammar' and+-- 'PEG.QQ.pegRules' run "PEG.Analysis" at splice time: it computes each+-- rule's nullability and FIRST set and rejects a grammar in which any rule+-- can begin with itself, naming the rule and the chain of head references+-- that closes the cycle.  It also rejects a repetition of something nullable,+-- an undefined non-terminal and a duplicate rule.  That happens once, when+-- the grammar is written, in milliseconds.+--+-- It used to happen again, and differently, on every compilation of every+-- module that mentioned the grammar: entries of the environment carried the+-- FIRST set as type-level data and an @Acyclic@ constraint checked that no+-- rule was in its own.  "PEG.Type" records what that cost — it was the whole+-- cost of a large grammar — and why it is gone.+--+-- What is given up is the case the splice cannot see:+--+-- * 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.+-- * '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+--   therefore reported by neither.  A grammar written as one+--   'PEG.QQ.pegGrammar' has no such gap: it is closed-world, so every+--   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. module PEG.Grammar   ( Rules (..)   , Grammar (..)-  , Acyclic   ) where -import Data.Kind    (Constraint, Type)-import GHC.TypeLits (ErrorMessage (..), Symbol, TypeError)+import Data.Kind    (Type)  import PEG.Syntax  (Name, PExp)-import PEG.TyLevel (Elem) import PEG.Type  -- | A typed, heterogeneous list of named grammar rules.@@ -35,38 +60,25 @@ data Rules (s :: Type) (env :: Env) (defs :: Env) where   RNil  :: Rules s env '[]   RCons :: Name n-        -> PExp s env ty a+        -> PExp s env a         -> Rules s env rest-        -> Rules s env ('(n, 'EnvEntry ty a) ': rest)--type family Acyclic (env :: Env) :: Constraint where-  Acyclic '[]                                      = ()-  Acyclic ('(s, 'EnvEntry ('MkTy _ f) _) ': rest) =-    (NotLeftRec s (Elem s f) f, Acyclic rest)--type family NotLeftRec (s :: Symbol) (b :: Bool)-                       (f :: [Symbol]) :: Constraint where-  NotLeftRec _ 'False _ = ()-  NotLeftRec s 'True  f =-    TypeError ('Text "Left-recursive non-terminal: " ':<>: 'ShowType s-         ':$$: 'Text "Its head set already contains itself: "-               ':<>: 'ShowType f-         ':$$: 'Text "Violates the acyclicity condition i `notElem` Gamma(i).F.")+        -> 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.+-- -- A 'Grammar' is monomorphic in its stream.  To reuse one grammar across -- several stream types, give it a signature of the form--- @forall s. 'PEG.Stream.Stream' s => Grammar s Env ty a@ — but note that+-- @forall s. 'PEG.Stream.Stream' s => Grammar s Env a@ — but note that -- doing so turns the value into a function of a dictionary, so the compiled -- parser is no longer shared between calls.  Prefer a monomorphic top-level -- signature.------ Constructing a 'Grammar' value discharges the 'Acyclic' constraint, so--- any left-recursion in @env@ becomes a compile-time type error.-data Grammar (s :: Type) (env :: Env) (startTy :: Ty) (startA :: Type) where-  Grammar :: Acyclic env-          => Rules s env env-          -> PExp s env startTy startA-          -> Grammar s env startTy startA+data Grammar (s :: Type) (env :: Env) (a :: Type) where+  Grammar :: Rules s env env+          -> PExp s env a+          -> Grammar s env a
src/PEG/Member.hs view
@@ -18,15 +18,19 @@ -- materialised from type information at runtime, enabling non-terminal lookup -- during parsing. ----- == Why the 'PEG.Type.Ty' is not an index+-- == Why the class has only the indices it has ----- The witness deliberately does /not/ record the non-terminal's--- 'PEG.Type.Ty'.  Resolving @KnownMember s env a@ walks @env@ one instance at--- a time, and every index of the class is carried along — and re-normalised —--- at each of those steps.  A 'PEG.Type.Ty' carries a FIRST set, so an index--- for it makes each step cost @O(|env|)@ instead of @O(1)@.  Nothing needs--- it: 'Here' binds the entry's @ty@ existentially, which is enough to pull--- the matching rule out of a rule table.+-- Resolving @KnownMember s env a@ walks @env@ one instance at a time, and+-- every index of the class is carried along — and re-normalised — at each of+-- those steps.  An index whose size grows with the grammar therefore makes+-- each step cost @O(|env|)@ instead of @O(1)@.  Entries used to carry a FIRST+-- set for exactly that reason, and keeping it out of this class was worth a+-- large constant; it is now out of the environment altogether (see+-- "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. module PEG.Member   ( Member (..)   , KnownMember (..)@@ -41,7 +45,7 @@ -- | @'Member' s env a@ witnesses that @env@ binds the name @s@ to a rule -- returning @a@, and records /where/ in @env@ that binding is. data Member (s :: Symbol) (env :: Env) (a :: Type) where-  Here  :: Member s ('(s, 'EnvEntry ty a) ': rest) a+  Here  :: Member s ('(s, 'EnvEntry a) ': rest) a   There :: Member s rest a -> Member s (e ': rest) a  class KnownMember (s :: Symbol) (env :: Env) (a :: Type) where@@ -62,9 +66,9 @@ class KnownMemberStep (o :: Ordering) (s :: Symbol) (env :: Env) (a :: Type) where   memberStep :: Proxy o -> Member s env a --- The entry is taken apart in the instance head, so @ty@ is bound by--- matching and never has to be threaded through the class.-instance (s ~ t) => KnownMemberStep 'EQ s ('(t, 'EnvEntry ty a) ': rest) a where+-- The entry is taken apart in the instance head, so the result type is bound+-- by matching and never has to be threaded through the class.+instance (s ~ t) => KnownMemberStep 'EQ s ('(t, 'EnvEntry a) ': rest) a where   memberStep _ = Here  instance KnownMember s rest a => KnownMemberStep 'LT s ('(t, e) ': rest) a where
src/PEG/Parse.hs view
@@ -132,20 +132,20 @@   }  -- | Run a grammar with 'defaultOpts'.-parse :: Stream s => Grammar s env ty a -> s -> Result s a+parse :: Stream s => Grammar s env a -> s -> Result s a parse = parseWith defaultOpts {-# INLINABLE parse #-}-{-# SPECIALIZE parse :: Grammar String env ty a -> String -> Result String a #-}-{-# SPECIALIZE parse :: Grammar T.Text env ty a -> T.Text -> Result T.Text a #-}+{-# SPECIALIZE parse :: Grammar String env a -> String -> Result String a #-}+{-# SPECIALIZE parse :: Grammar T.Text env a -> T.Text -> Result T.Text a #-} {-# SPECIALIZE parse-      :: Grammar B.ByteString env ty a -> B.ByteString -> Result B.ByteString a #-}+      :: Grammar B.ByteString env 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 ty a.-             Stream s => Opts -> Grammar s env ty a -> s -> Result s a+parseWith :: forall s env a.+             Stream s => Opts -> Grammar s env a -> s -> Result s a parseWith opts g = run   where     step = compileGrammar (optTabWidth opts) g@@ -156,11 +156,11 @@       (# | (# a, st #) #) -> OK a (takeS (stOff st) input) (stInput st) {-# INLINABLE parseWith #-} {-# SPECIALIZE parseWith-      :: Opts -> Grammar String env ty a -> String -> Result String a #-}+      :: Opts -> Grammar String env a -> String -> Result String a #-} {-# SPECIALIZE parseWith-      :: Opts -> Grammar T.Text env ty a -> T.Text -> Result T.Text a #-}+      :: Opts -> Grammar T.Text env a -> T.Text -> Result T.Text a #-} {-# SPECIALIZE parseWith-      :: Opts -> Grammar B.ByteString env ty a+      :: Opts -> Grammar B.ByteString env a       -> B.ByteString -> Result B.ByteString a #-}  --------------------------------------------------------------------------------@@ -174,7 +174,7 @@   CNil  :: CRules s env '[]   CCons :: Step s a         -> CRules s env rest-        -> CRules s env ('(n, 'EnvEntry ty a) ': rest)+        -> CRules s env ('(n, 'EnvEntry a) ': rest)  clookup :: Member n defs a -> CRules s env defs -> Step s a clookup Here      (CCons f _)    = f@@ -185,8 +185,8 @@ -- The traversal resolves every non-terminal reference to the corresponding -- compiled rule, so at parse time a non-terminal costs one indirect call -- instead of a walk down the rule list.-compileGrammar :: forall s env ty a.-                  Stream s => Int -> Grammar s env ty a -> Step s a+compileGrammar :: forall s env a.+                  Stream s => Int -> Grammar s env a -> Step s a compileGrammar tw (Grammar rules start) = compileE tw table start   where     table :: CRules s env env@@ -201,11 +201,11 @@ -- per-character path stops being allocation-free.  Callers using another -- stream should mark their own monomorphic parser bindings INLINABLE. {-# SPECIALIZE compileGrammar-      :: Int -> Grammar String env ty a -> Step String a #-}+      :: Int -> Grammar String env a -> Step String a #-} {-# SPECIALIZE compileGrammar-      :: Int -> Grammar T.Text env ty a -> Step T.Text a #-}+      :: Int -> Grammar T.Text env a -> Step T.Text a #-} {-# SPECIALIZE compileGrammar-      :: Int -> Grammar B.ByteString env ty a -> Step B.ByteString a #-}+      :: Int -> Grammar B.ByteString env 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,8 +213,8 @@ simpleCS :: CharSet -> Bool simpleCS cs = not (memberCS '\n' cs) && not (memberCS '\t' cs) -compileE :: forall s env ty a.-            Stream s => Int -> CRules s env env -> PExp s env ty a -> Step s a+compileE :: forall s env a.+            Stream s => Int -> CRules s env env -> PExp s env a -> Step s a compileE tw table = comp   where     -- Select the stream operations once per compiled grammar.  Leaving them@@ -228,7 +228,7 @@     !packS   = packString    :: String -> s     !emptyS  = packS [] -    comp :: forall t b. PExp s env t b -> Step s b+    comp :: forall b. PExp s env b -> Step s b      comp (Pure x) = \_ st -> (# | (# x, st #) #) @@ -245,10 +245,14 @@     comp (Span  cs) = spanChunk (\c -> memberCS c cs) (simpleCS cs) False     comp (Span1 cs) = spanChunk (\c -> memberCS c cs) (simpleCS cs) True -    -- 'ty' and 'a' come from the constructor's own equality-    -- @Lookup n env ~ 'EnvEntry ty a@, so no type family has to be reduced+    -- '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      -- 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
@@ -28,390 +28,99 @@ module PEG.QQ   ( pegExpr   , pegRules+  , pegGrammar   ) where  import Control.Monad              (foldM)-import Data.List                  (nub)+import Data.List                  (elemIndex, nub) import Language.Haskell.TH        (Exp (..), Pat (..), Q) import qualified Language.Haskell.TH      as TH import Language.Haskell.TH.Quote  (QuasiQuoter (..))  import PEG-import PEG.QQ.HsExp (parseHsExp)--data Def = Def String PExpr-  deriving Show--data Item = Item (Maybe String) PExpr-  deriving Show--data PExpr-  = EChoice  [PExpr]-  | ESeq     [Item] (Maybe String)-  | EAnd     PExpr-  | ENot     PExpr-  | EOpt     PExpr-  | EStar    PExpr-  | EPlus    PExpr-  | EChar    Char-  | EString  String-  | EClass   Bool [(Char,Char)]   -- ^ 'True' when the class is negated.-  | EDot-  | ENT      String-  | EIndent  RelS PExpr-  | EPos     RelS PExpr-  | EAlign   PExpr-  deriving Show--data RelS-  = RGt-  | RGe-  | REq-  | RAny-  | ROffset Int-  | RNamed  String-  deriving Show--type P a = String -> Either String (a, String)--errorAt :: String -> String -> Either String a-errorAt msg s = Left $ msg ++ " at: " ++ show (take 30 s)--spaces :: String -> String-spaces []         = []-spaces ('#':xs)   = spaces (drop 1 (dropWhile (/= '\n') xs))-spaces (c:xs)-  | c == ' ' || c == '\t' || c == '\n' || c == '\r' = spaces xs-  | otherwise = c:xs--tok :: String -> P ()-tok t s = case stripPrefix t (spaces s) of-  Just r  -> Right ((), r)-  Nothing -> errorAt ("expected " ++ show t) s-  where-    stripPrefix [] xs                 = Just xs-    stripPrefix (p:ps) (x:xs) | p==x  = stripPrefix ps xs-    stripPrefix _ _                   = Nothing--ident :: P String-ident s0 = case spaces s0 of-  (c:xs) | isIdStart c ->-    let (rest, leftover) = span isIdCont xs-    in Right (c:rest, leftover)-  s -> errorAt "expected identifier" s-  where-    isIdStart c = c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')-    isIdCont c  = isIdStart c || (c >= '0' && c <= '9')--charLit :: P Char-charLit s0 = case spaces s0 of-  ('\'':xs) -> do (c, r1) <- escChar '\'' xs-                  case r1 of-                    ('\'':r2) -> Right (c, r2)-                    _         -> errorAt "expected closing '" r1-  s         -> errorAt "expected character literal" s--strLit :: P String-strLit s0 = case spaces s0 of-  ('"':xs) -> loop xs-  s        -> errorAt "expected string literal" s-  where-    loop ('"':r) = Right ("", r)-    loop r0      = do (c, r1) <- escChar '"' r0-                      (cs, r2) <- loop r1-                      pure (c:cs, r2)--escChar :: Char -> P Char-escChar _ ('\\':e:xs) = case e of-  'n'  -> Right ('\n', xs)-  't'  -> Right ('\t', xs)-  'r'  -> Right ('\r', xs)-  '\\' -> Right ('\\', xs)-  '\'' -> Right ('\'', xs)-  '"'  -> Right ('"',  xs)-  '['  -> Right ('[',  xs)-  ']'  -> Right (']',  xs)-  '0'  -> Right ('\0', xs)-  '^'  -> Right ('^',  xs)-  _    -> errorAt ("unknown escape \\" ++ [e]) xs-escChar stopC (c:xs)-  | c == stopC = errorAt "unexpected close quote" (c:xs)-  | otherwise  = Right (c, xs)-escChar _ [] = Left "unexpected end of input in literal"---- | A character class.  A leading @^@ negates it, as in POSIX; write--- @[\\^]@ for a class containing the caret itself.-classLit :: P (Bool, [(Char, Char)])-classLit s0 = case spaces s0 of-  ('[':'^':xs) -> do-    (rs, r) <- loop xs-    if null rs-      then errorAt "empty negated character class" s0-      else Right ((True, rs), r)-  ('[':xs)     -> do-    (rs, r) <- loop xs-    Right ((False, rs), r)-  s            -> errorAt "expected character class" s-  where-    loop (']':r) = Right ([], r)-    loop []      = Left "unterminated character class"-    loop r0      = do-      (c1, r1) <- escChar ']' r0-      case r1 of-        ('-':']':r2) -> pure ([(c1, c1), ('-', '-')], r2)-        ('-':r2) ->-          do (c2, r3) <- escChar ']' r2-             (rs, r4) <- loop r3-             pure ((c1, c2) : rs, r4)-        _ ->-          do (rs, r2) <- loop r1-             pure ((c1, c1) : rs, r2)--actionLit :: P String-actionLit s0 = case spaces s0 of-  ('{':xs) -> go (1 :: Int) ' ' [] xs-  s        -> errorAt "expected a semantic action" s-  where-    go _ _ _ [] = Left "unterminated semantic action: missing '}'"-    go n prev acc s = case s of-      ('{':'-':r) -> do-        (com, r') <- blockComment (1 :: Int) r-        go n '}' (revApp ("{-" ++ com) acc) r'-      ('"':r) -> do-        (str, r') <- literalBody '"' r-        go n '"' (revApp ('"' : str) acc) r'-      ('\'':r) | not (isIdChar prev) -> do-        (ch, r') <- literalBody '\'' r-        go n '\'' (revApp ('\'' : ch) acc) r'-      ('{':r) -> go (n + 1) '{' ('{' : acc) r-      ('}':r) | n == 1    -> Right (reverse acc, r)-              | otherwise -> go (n - 1) '}' ('}' : acc) r-      (c:_) | c `elem` symChars ->-        let (sym, r) = span (`elem` symChars) s-        in if all (== '-') sym && length sym >= 2-             then let (line, r') = span (/= '\n') r-                  in go n '\n' (revApp (sym ++ line) acc) r'-             else go n (last sym) (revApp sym acc) r-      (c:r) -> go n c (c : acc) r--    isIdChar c = c == '_' || c == '\''-              || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')-              || (c >= '0' && c <= '9')--    literalBody _ [] = Left "unterminated literal in a semantic action"-    literalBody q ('\\':c:r)     = do (b, r') <- literalBody q r-                                      Right ('\\' : c : b, r')-    literalBody q (c:r) | c == q = Right ([c], r)-    literalBody q (c:r)          = do (b, r') <- literalBody q r-                                      Right (c : b, r')--    blockComment _ []              = Left "unterminated {- -} comment in a semantic action"-    blockComment k ('-':'}':r)-      | k == 1                     = Right ("-}", r)-      | otherwise                  = do (c, r') <- blockComment (k - 1) r-                                        Right ("-}" ++ c, r')-    blockComment k ('{':'-':r)     = do (c, r') <- blockComment (k + 1) r-                                        Right ("{-" ++ c, r')-    blockComment k (c:r)           = do (c', r') <- blockComment k r-                                        Right (c : c', r')--    revApp xs acc = reverse xs ++ acc--    symChars = "!#$%&*+./<=>?@\\^|-~:"--parseExpr :: P PExpr-parseExpr s0 = do-  (e1, s1) <- parseSeq s0-  loop [e1] s1-  where-    loop acc s = case tok "/" s of-      Right (_, s') -> do (e, s'') <- parseSeq s'-                          loop (e:acc) s''-      Left _        -> case reverse acc of-        [x] -> Right (x, s)-        xs  -> Right (EChoice xs, s)--parseSeq :: P PExpr-parseSeq s0 = loop [] s0-  where-    loop acc s = case parseLabelled s of-      Right (it, s') -> loop (it:acc) s'-      Left _         -> case actionLit s of-        Right (act, s') -> Right (ESeq (reverse acc) (Just act), s')-        Left _          -> Right (ESeq (reverse acc) Nothing,    s)--parseLabelled :: P Item-parseLabelled s = case label s of-  Just (l, s1) -> do (e, s2) <- parsePrefix s1-                     pure (Item (Just l) e, s2)-  Nothing      -> do (e, s1) <- parsePrefix s-                     pure (Item Nothing e, s1)-  where-    label s' = case ident s' of-      Right (name, s1) -> case tok ":" s1 of-        Right (_, s2) -> Just (name, s2)-        Left _        -> Nothing-      Left _ -> Nothing--parsePrefix :: P PExpr-parsePrefix s = case tok "&" s of-  Right (_, s') -> do (e, s'') <- parseSuffix s'; pure (EAnd e, s'')-  Left _        -> case tok "!" s of-    Right (_, s') -> do (e, s'') <- parseSuffix s'; pure (ENot e, s'')-    Left _        -> parseSuffix s--parseSuffix :: P PExpr-parseSuffix s = do-  (p, s1) <- parsePrimary s-  loop p s1-  where-    loop p s1 = case tok "?" s1 of-      Right (_, s2) -> loop (EOpt p) s2-      Left _ -> case tok "*" s1 of-        Right (_, s2) -> loop (EStar p) s2-        Left _ -> case tok "+" s1 of-          Right (_, s2) -> loop (EPlus p) s2-          Left _ -> case indented EIndent "^" p s1 of-            Right (p', s2) -> loop p' s2-            Left _ -> case indented EPos "_" p s1 of-              Right (p', s2) -> loop p' s2-              Left _         -> Right (p, s1)--    indented con marker p s1 = do-      (_, s2) <- tok marker s1-      (r, s3) <- parseRel s2-      pure (con r p, s3)--parseRel :: P RelS-parseRel s = case tok ">=" s of-  Right (_, s1) -> Right (RGe, s1)-  Left _ -> case tok ">" s of-    Right (_, s1) -> Right (RGt, s1)-    Left _ -> case tok "=" s of-      Right (_, s1) -> Right (REq, s1)-      Left _ -> case tok "~" s of-        Right (_, s1) -> Right (RAny, s1)-        Left _ -> case tok "@" s of-          Right (_, s1) -> do (name, s2) <- ident s1-                              pure (RNamed name, s2)-          Left _ -> case tok "+" s of-            Right (_, s1) -> case span isDigit (spaces s1) of-              ([], _)     -> errorAt "expected a number after '+'" s1-              (ds, s2)    -> Right (ROffset (read ds), s2)-            Left _ -> errorAt "expected an indentation relation" s-  where-    isDigit c = c >= '0' && c <= '9'--parsePrimary :: P PExpr-parsePrimary s =-  case tok "(" s of-    Right (_, s1) -> do (e, s2) <- parseExpr s1-                        (_, s3) <- tok ")" s2-                        pure (e, s3)-    Left _ -> case parseAlign s of-     Right r -> Right r-     Left _ -> case tok "." s of-      Right (_, s1) -> Right (EDot, s1)-      Left _ -> case charLit s of-        Right (c, s1) -> Right (EChar c, s1)-        Left _ -> case strLit s of-          Right (cs, s1) -> Right (EString cs, s1)-          Left _ -> case classLit s of-            Right ((neg, rs), s1) -> Right (EClass neg rs, s1)-            Left _ -> case ident s of-              Right (name, s1) ->-                case tok "<-" s1 of-                  Right _  -> errorAt "definition where expression expected" s-                  Left _   -> Right (ENT name, s1)-              Left _ -> errorAt "expected primary expression" s--parseAlign :: P PExpr-parseAlign s = do-  (_, s1) <- tok "|" s-  (e, s2) <- parseExpr s1-  if isEmptyExpr e-    then errorAt "empty alignment: write |e| with a non-empty e" s-    else do (_, s3) <- tok "|" s2-            pure (EAlign e, s3)-  where-    isEmptyExpr (ESeq [] Nothing) = True-    isEmptyExpr _                 = False+import PEG.Analysis  (Diagnostic (..), World (..), analyse, analyseWith,+                      renderDiagnostic, spannable)+import PEG.QQ.HsExp  (parseHsExp, parseHsType)+import PEG.QQ.Syntax (Def (..), Directive (..), Item (..), PExpr (..),+                      RelS (..), parseDirectives, parseExpr, parseGrammar,+                      spaces) -parseGrammar :: P [Def]-parseGrammar s0 = loop [] s0+-- | Translate a DSL expression, given a way to emit a reference to a+-- non-terminal.+--+-- The two quasi-quoters differ in exactly that: 'pegRules' emits+-- @nt \@"name"@, which makes GHC search the environment, while 'pegGrammar'+-- knows every rule's position and emits @ntw \@"name" witness@, which does+-- not.  Everything else about the translation is shared, so the two cannot+-- drift.+translateExprWith :: (String -> Q Exp) -> PExpr -> Q Exp+translateExprWith ntRef = go   where-    loop acc s = case ident s of-      Left _ -> case spaces s of-        [] -> Right (reverse acc, "")-        s' -> errorAt "expected definition or end of input" s'-      Right (name, s1) -> do-        (_, s2)  <- tok "<-" s1-        (e, s3)  <- parseExpr s2-        loop (Def name e : acc) s3+    go (EChar c) =+      [| Term c |]+    go EDot =+      [| AnyChar |]+    go (ENT name) = ntRef name+    go (EString str)+      | null str  = [| pureP "" |]+      | otherwise = [| stringNE str |]+    go (EClass neg rs)+      -- A character class becomes a single 'Sat' node holding a compact+      -- 'PEG.CharSet.CharSet'.  Expanding it into a chain of ordered choices, as+      -- an earlier version did, made matching one character of @[a-zA-Z0-9_]@+      -- cost 63 parser steps.+      | neg       = [| notCharClass rs |]+      | otherwise = [| charClass rs |]+    go (EAnd e)  = do+      e' <- go e+      [| Not (Not $(pure e')) |]+    go (ENot e)  = do+      e' <- go e+      [| Not $(pure e') |]+    go (EOpt e)  = do+      e' <- go e+      [| opt $(pure e') |]+    -- A repetition of a single character -- @[a-z]*@, @','+@, @.*@ -- compiles to+    -- one 'PEG.Syntax.Span' node and produces a /chunk of the input stream/: a+    -- 'Data.Text.Text' slice rather than a @['Char']@.  Only a bare class, literal+    -- or dot qualifies; a wrapper such as @[a-z]^>*@ changes the meaning of each+    -- iteration, so those keep the generic 'Star'.+    go (EStar (EClass neg rs))+      | neg       = [| spanOf (notInRanges rs) |]+      | otherwise = [| spanOf (fromRanges rs) |]+    go (EStar (EChar c)) = [| spanOf (singletonCS c) |]+    go (EStar EDot)      = [| spanOf anyCS |]+    go (EPlus (EClass neg rs))+      | neg       = [| spanOf1 (notInRanges rs) |]+      | otherwise = [| spanOf1 (fromRanges rs) |]+    go (EPlus (EChar c)) = [| spanOf1 (singletonCS c) |]+    go (EPlus EDot)      = [| spanOf1 anyCS |]+    go (EStar e) = do+      e' <- go e+      [| Star $(pure e') |]+    go (EPlus e) = do+      e' <- go e+      [| plus $(pure e') |]+    go (EIndent r e) = do+      e' <- go e+      [| Indent $(translateRel r) $(pure e') |]+    go (EPos r e) = do+      e' <- go e+      [| Position $(translateRel r) $(pure e') |]+    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 -translateExpr :: PExpr -> Q Exp-translateExpr (EChar c) =-  [| Term c |]-translateExpr EDot =-  [| AnyChar |]-translateExpr (ENT name) =-  pure $ TH.AppTypeE (TH.VarE 'nt) (TH.LitT (TH.StrTyLit name))-translateExpr (EString s)-  | null s    = [| pureP "" |]-  | otherwise = [| stringNE s |]-translateExpr (EClass neg rs)-  -- A character class becomes a single 'Sat' node holding a compact-  -- 'PEG.CharSet.CharSet'.  Expanding it into a chain of ordered choices, as-  -- an earlier version did, made matching one character of @[a-zA-Z0-9_]@-  -- cost 63 parser steps.-  | neg       = [| notCharClass rs |]-  | otherwise = [| charClass rs |]-translateExpr (EAnd e)  = do-  e' <- translateExpr e-  [| Not (Not $(pure e')) |]-translateExpr (ENot e)  = do-  e' <- translateExpr e-  [| Not $(pure e') |]-translateExpr (EOpt e)  = do-  e' <- translateExpr e-  [| opt $(pure e') |]--- A repetition of a single character -- @[a-z]*@, @','+@, @.*@ -- compiles to--- one 'PEG.Syntax.Span' node and produces a /chunk of the input stream/: a--- 'Data.Text.Text' slice rather than a @['Char']@.  Only a bare class, literal--- or dot qualifies; a wrapper such as @[a-z]^>*@ changes the meaning of each--- iteration, so those keep the generic 'Star'.-translateExpr (EStar (EClass neg rs))-  | neg       = [| spanOf (notInRanges rs) |]-  | otherwise = [| spanOf (fromRanges rs) |]-translateExpr (EStar (EChar c)) = [| spanOf (singletonCS c) |]-translateExpr (EStar EDot)      = [| spanOf anyCS |]-translateExpr (EPlus (EClass neg rs))-  | neg       = [| spanOf1 (notInRanges rs) |]-  | otherwise = [| spanOf1 (fromRanges rs) |]-translateExpr (EPlus (EChar c)) = [| spanOf1 (singletonCS c) |]-translateExpr (EPlus EDot)      = [| spanOf1 anyCS |]-translateExpr (EStar e) = do-  e' <- translateExpr e-  [| Star $(pure e') |]-translateExpr (EPlus e) = do-  e' <- translateExpr e-  [| plus $(pure e') |]-translateExpr (EIndent r e) = do-  e' <- translateExpr e-  [| Indent $(translateRel r) $(pure e') |]-translateExpr (EPos r e) = do-  e' <- translateExpr e-  [| Position $(translateRel r) $(pure e') |]-translateExpr (EAlign e) = do-  e' <- translateExpr e-  [| Align $(pure e') |]-translateExpr (EChoice es) = case es of-  []       -> fail "QQ: empty choice (should be impossible)"-  (e:rest) -> do-    e'    <- translateExpr e-    rest' <- mapM translateExpr rest-    foldM (\acc x -> [| $(pure acc) .||. $(pure x) |]) e' rest'-translateExpr (ESeq items act) = translateSeq items act+-- | 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)))  translateRel :: RelS -> Q Exp translateRel RGt          = [| gtR |]@@ -421,8 +130,8 @@ translateRel (ROffset n)  = [| offsetR n |] translateRel (RNamed nm)  = pure (TH.VarE (TH.mkName nm)) -translateSeq :: [Item] -> Maybe String -> Q Exp-translateSeq items act = do+translateSeqWith :: (String -> Q Exp) -> [Item] -> Maybe String -> Q Exp+translateSeqWith ntRef items act = do   let labels = [ l | Item (Just l) _ <- items ]   case duplicates labels of     (l:_) -> fail ("QQ: the label " ++ show l@@ -433,7 +142,7 @@     Just src -> case parseHsExp src of       Right e  -> pure e       Left err -> fail ("QQ: in the semantic action {" ++ src ++ "}: " ++ err)-  es <- mapM (\(Item _ e) -> translateExpr e) items+  es <- mapM (\(Item _ e) -> translateExprWith ntRef e) items   case es of     []       -> [| pureP $(pure body) |]     (e:rest) -> do@@ -450,11 +159,11 @@      duplicates xs = [ x | x <- nub xs, length (filter (== x) xs) > 1 ] -translateRules :: [Def] -> Q Exp-translateRules [] = [| RNil |]-translateRules (Def name expr : rest) = do-  body  <- translateExpr expr-  rest' <- translateRules rest+translateRules :: (String -> Q Exp) -> [Def] -> Q Exp+translateRules _ [] = [| RNil |]+translateRules ntRef (Def name _ expr : rest) = do+  body  <- translateExprWith ntRef expr+  rest' <- translateRules ntRef rest   let nameProxy = TH.AppTypeE (TH.ConE 'Name) (TH.LitT (TH.StrTyLit name))   [| RCons $(pure nameProxy) $(pure body) $(pure rest') |] @@ -474,7 +183,7 @@ pegExprExp src = case parseExpr src of   Left err     -> fail ("pegExpr: parse error: " ++ err)   Right (e, rest) -> case spaces rest of-    []  -> translateExpr e+    []  -> translateExprWith ntByName e     leftover -> fail ("pegExpr: unconsumed input: " ++ show (take 30 leftover))  -- | Quasi-quoter for a set of named PEG rules.@@ -505,4 +214,288 @@ pegRulesExp :: String -> Q Exp pegRulesExp src = case parseGrammar src of   Left err -> fail ("pegRules: parse error: " ++ err)-  Right (defs, _) -> translateRules defs+  Right (defs, _) ->+    -- Left recursion, a nullable repetition and a duplicate rule, reported+    -- here because nothing else reports them any more: the FIRST sets that+    -- @Acyclic@ used to check are no longer in the types.  The block is+    -- analysed 'Open' because it may be only part of a rule set — see+    -- 'PEG.Analysis.World' — so a cycle that closes across two blocks is+    -- caught by neither this nor GHC.  'pegGrammar' has no such gap.+    case analyseWith Open defs of+      Left ds -> fail ("pegRules:\n" ++ unlines+                         -- six spaces, so the body lines up under the bullet+                         -- GHC puts in front of the first line+                         [ "      " ++ l | d <- ds, l <- lines (renderDiagnostic d) ])+      Right _ -> translateRules ntByName defs++--------------------------------------------------------------------------------+-- pegGrammar: a whole grammar, environment included+--------------------------------------------------------------------------------++-- | 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   \<- ...+--         |]+-- @+--+-- == 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+-- signature:+--+-- @+-- [pegGrammar|+--   %name  arith+--   %start expr+--   expr   :: Exp \<- t:term ts:(o:[+-] u:term)* { foldl addOp t ts }+--   term   :: Exp \<- ...+-- |]+-- @+--+-- 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.+--+-- == 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.+-- [@%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.+pegGrammar :: QuasiQuoter+pegGrammar = QuasiQuoter+  { quoteExp  = pegGrammarExp+  , quoteDec  = pegGrammarDec+  , quotePat  = \_ -> fail "pegGrammar: cannot be used as a pattern"+  , quoteType = \_ -> fail "pegGrammar: cannot be used as a type"+  }++-- | A grammar that has been parsed and checked: the pieces both forms need.+--+-- The analysis's own result is not among them.  It used to be — the FIRST+-- sets it computes were written into the environment — and now that entries+-- carry only a result type, running it is entirely a matter of the+-- diagnostics it raises.  It is still run, and it is now the only thing that+-- rejects a left-recursive grammar; see "PEG.Grammar".+data GrammarSrc = GrammarSrc+  { gsDirs  :: [Directive]+  , gsDefs  :: [Def]+  , gsStart :: PExpr+  }++gsNames :: GrammarSrc -> [String]+gsNames gs = [ n | Def n _ _ <- gsDefs gs ]++-- | Parse the header, the rules and the start expression, and run the+-- analysis over all of them.+parseGrammarSrc :: String -> Q GrammarSrc+parseGrammarSrc src = do+  (dirs, afterDirs) <- orFail (parseDirectives src)+  -- A mistyped directive is silent otherwise: @%strt expr@ would be reported+  -- as a missing %start, which points at the wrong thing.+  case [ k | Directive k _ <- dirs, k `notElem` knownDirectives ] of+    []    -> pure ()+    (k:_) -> fail ("pegGrammar: unknown directive %" ++ k+                     ++ "\n      known directives are "+                     ++ unwords [ '%' : d | d <- knownDirectives ])+  (defs, leftover)  <- orFail (parseGrammar afterDirs)+  case spaces leftover of+    [] -> pure ()+    r  -> fail ("pegGrammar: unconsumed input: " ++ show (take 30 r))+  startSrc <- case directive "start" dirs of+    Just v  -> pure v+    Nothing -> fail "pegGrammar: no %start directive"+  (start0, startRest) <- orFail (parseExpr startSrc)+  let start = normaliseStart start0+  case spaces startRest of+    [] -> pure ()+    r  -> fail ("pegGrammar: unconsumed input in %start: " ++ show (take 30 r))+  -- The start expression is a rule body in every way that matters here, so it+  -- is checked with the others: a name it references and no rule defines is+  -- reported the same way.+  case analyse (Def "%start" Nothing start : defs) of+    Left ds  -> fail ("pegGrammar:\n" ++ unlines+                        [ "      " ++ l+                        | d <- ds, l <- lines (renderDiagnostic (unstart d)) ])+    Right _  -> pure ()+  pure (GrammarSrc dirs defs start)+  where+    orFail = either (\e -> fail ("pegGrammar: parse error: " ++ e)) pure++    -- The start expression is not a rule, so it should not be named as one.+    unstart (LeftRecursive n p)  = LeftRecursive (rename n) (map rename p)+    unstart (NullableStar n)     = NullableStar (rename n)+    unstart (UndefinedNT n ns)   = UndefinedNT n (filter (/= "%start") ns)+    unstart (DuplicateRule n)    = DuplicateRule (rename n)+    rename n = if n == "%start" then "the start expression" else n++-- | @%start expr@ means the expression @expr@, not a one-item sequence whose+-- value is discarded.+--+-- Inside a rule, @r \<- term@ with neither a label nor an action does return+-- @()@ — that is the DSL's rule and it stays.  But a start expression is not+-- a rule: it is the @(nt \@"expr")@ that used to be written out by hand next+-- to the rule set, and that returned the rule's value.  A start with a label+-- or an action of its own is left alone; only a lone unlabelled item is+-- unwrapped.+normaliseStart :: PExpr -> PExpr+normaliseStart (ESeq [Item Nothing e] Nothing) = e+normaliseStart e                               = e++knownDirectives :: [String]+knownDirectives = ["start", "name", "env", "stream", "result"]++directive :: String -> [Directive] -> Maybe String+directive k ds = case [ v | Directive k' v <- ds, k' == k ] of+  (v:_) -> Just v+  []    -> Nothing++-- | Emit @ntw \@"name" (There (... Here))@: the proof instead of the search.+ntByWitness :: [String] -> String -> Q Exp+ntByWitness names name = case elemIndex name names of+  Nothing -> fail ("pegGrammar: undefined non-terminal: " ++ name)+  Just k  -> pure (TH.AppE (TH.AppTypeE (TH.VarE 'ntw)+                                        (TH.LitT (TH.StrTyLit name)))+                           (witness k))+  where+    witness 0 = TH.ConE 'Here+    witness k = TH.AppE (TH.ConE 'There) (witness (k - 1))++pegGrammarExp :: String -> Q Exp+pegGrammarExp src = do+  gs <- parseGrammarSrc src+  let ntRef = ntByWitness (gsNames gs)+  rules <- translateRules ntRef (gsDefs gs)+  start <- translateExprWith ntRef (gsStart gs)+  [| Grammar $(pure rules) $(pure start) |]++pegGrammarDec :: String -> Q [TH.Dec]+pegGrammarDec src = do+  gs <- parseGrammarSrc src+  gname <- case directive "name" (gsDirs gs) of+    Just v  -> pure (TH.mkName 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)))+      streamV  = TH.mkName "s"+  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 ]+  startRes <- case directive "result" (gsDirs gs) of+    Just t  -> either (\e -> fail ("pegGrammar: in %result: " ++ e)) pure+                      (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) []]+       ]+  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.+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\+                    \  Declaring " ++ show gname ++ " means writing the \+                    \environment down, and a rule's\n  result type is the one \+                    \thing the grammar does not say: write\n    " ++ n+                    ++ " :: T <- ...")+  Just src -> case parseHsType src of+    Left e  -> fail ("pegGrammar: in the result type of " ++ n ++ ": " ++ e)+    Right t -> pure (n, t)++-- | What the start expression returns, read off the rules' declared types.+--+-- This follows @translateSeqWith@: 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.+resultTypeOf :: TH.Type -> [(String, TH.Type)] -> PExpr -> Maybe TH.Type+resultTypeOf streamT anns = go+  where+    go (ENT n)       = lookup n anns+    go (EChar _)     = Just (TH.ConT ''Char)+    go EDot          = Just (TH.ConT ''Char)+    go (EClass _ _)  = Just (TH.ConT ''Char)+    go (EString _)   = Just (TH.ConT ''String)+    go (EAnd _)      = Just (TH.TupleT 0)+    go (ENot _)      = Just (TH.TupleT 0)+    go (EOpt e)      = TH.AppT (TH.ConT ''Maybe) <$> go e+    go (EStar e)     = rep e+    go (EPlus e)     = rep e+    go (EIndent _ e) = go e+    go (EPos _ e)    = go e+    go (EAlign e)    = go e+    go (EChoice es)  = firstJust (map go es)+    go (ESeq _ (Just _)) = Nothing+    go (ESeq items Nothing) = case [ e | Item (Just _) e <- items ] of+      []  -> Just (TH.TupleT 0)+      [e] -> go e+      es  -> foldl TH.AppT (TH.TupleT (length es)) <$> mapM go es++    rep e | spannable e = Just streamT+          | otherwise   = TH.AppT TH.ListT <$> go e++    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/HsExp.hs view
@@ -8,6 +8,7 @@ -- @base@ and @template-haskell@. module PEG.QQ.HsExp   ( parseHsExp+  , parseHsType   ) where  import Data.Char           (isAlpha, isAlphaNum, isDigit, isHexDigit,@@ -199,6 +200,21 @@   in (c ++ n, r)  type P a = [Tok] -> Either String (a, [Tok])++-- | Parse a type: what a @rule :: T@ annotation carries.  The grammar is the+-- one 'pType' already accepted inside an expression's @::@ annotation —+-- application, functions, lists and tuples — so nothing new is parsed here,+-- only reached from a new entry point.+parseHsType :: String -> Either String Type+parseHsType src = do+  toks <- lexHs src+  case toks of+    [] -> Left "empty type annotation"+    _  -> do+      (t, rest) <- pType toks+      case rest of+        [] -> Right t+        _  -> Left ("unconsumed input in type " ++ atTok rest)  parseHsExp :: String -> Either String Exp parseHsExp src = do
+ src/PEG/QQ/Syntax.hs view
@@ -0,0 +1,381 @@+-- | The concrete syntax of the grammar DSL: its abstract syntax tree and the+-- recursive-descent parser that produces it.+--+-- This is split out of "PEG.QQ" so that the tree has two consumers rather+-- than one.  "PEG.QQ" translates it to Template Haskell; "PEG.Analysis"+-- computes nullability, FIRST sets and the well-formedness diagnostics from+-- it, at splice time, without the type checker being involved.+module PEG.QQ.Syntax+  ( Def (..)+  , Item (..)+  , PExpr (..)+  , RelS (..)+  , Directive (..)+  , P+  , parseGrammar+  , parseDirectives+  , parseExpr+  , spaces+  ) where++-- | One rule: its name, the source of its @:: T@ result-type annotation if it+-- has one, and its body.+--+-- The annotation is what lets 'PEG.QQ.pegGrammar' write the environment down:+-- the nullability and FIRST set of a rule can be computed from the grammar,+-- but its result type cannot — that comes from the Haskell in its semantic+-- action, which GHC types long after the splice has run.+data Def = Def String (Maybe String) PExpr+  deriving Show++-- | A @%key value@ line in the header of a grammar: @%start@, @%name@,+-- @%env@, @%stream@, @%result@.  The value is the rest of the line.+data Directive = Directive String String+  deriving Show++data Item = Item (Maybe String) PExpr+  deriving Show++data PExpr+  = EChoice  [PExpr]+  | ESeq     [Item] (Maybe String)+  | EAnd     PExpr+  | ENot     PExpr+  | EOpt     PExpr+  | EStar    PExpr+  | EPlus    PExpr+  | EChar    Char+  | EString  String+  | EClass   Bool [(Char,Char)]   -- ^ 'True' when the class is negated.+  | EDot+  | ENT      String+  | EIndent  RelS PExpr+  | EPos     RelS PExpr+  | EAlign   PExpr+  deriving Show++data RelS+  = RGt+  | RGe+  | REq+  | RAny+  | ROffset Int+  | RNamed  String+  deriving Show++type P a = String -> Either String (a, String)++errorAt :: String -> String -> Either String a+errorAt msg s = Left $ msg ++ " at: " ++ show (take 30 s)++spaces :: String -> String+spaces []         = []+spaces ('#':xs)   = spaces (drop 1 (dropWhile (/= '\n') xs))+spaces (c:xs)+  | c == ' ' || c == '\t' || c == '\n' || c == '\r' = spaces xs+  | otherwise = c:xs++tok :: String -> P ()+tok t s = case stripPrefix t (spaces s) of+  Just r  -> Right ((), r)+  Nothing -> errorAt ("expected " ++ show t) s+  where+    stripPrefix [] xs                 = Just xs+    stripPrefix (p:ps) (x:xs) | p==x  = stripPrefix ps xs+    stripPrefix _ _                   = Nothing++ident :: P String+ident s0 = case spaces s0 of+  (c:xs) | isIdStart c ->+    let (rest, leftover) = span isIdCont xs+    in Right (c:rest, leftover)+  s -> errorAt "expected identifier" s+  where+    isIdStart c = c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')+    isIdCont c  = isIdStart c || (c >= '0' && c <= '9')++charLit :: P Char+charLit s0 = case spaces s0 of+  ('\'':xs) -> do (c, r1) <- escChar '\'' xs+                  case r1 of+                    ('\'':r2) -> Right (c, r2)+                    _         -> errorAt "expected closing '" r1+  s         -> errorAt "expected character literal" s++strLit :: P String+strLit s0 = case spaces s0 of+  ('"':xs) -> loop xs+  s        -> errorAt "expected string literal" s+  where+    loop ('"':r) = Right ("", r)+    loop r0      = do (c, r1) <- escChar '"' r0+                      (cs, r2) <- loop r1+                      pure (c:cs, r2)++escChar :: Char -> P Char+escChar _ ('\\':e:xs) = case e of+  'n'  -> Right ('\n', xs)+  't'  -> Right ('\t', xs)+  'r'  -> Right ('\r', xs)+  '\\' -> Right ('\\', xs)+  '\'' -> Right ('\'', xs)+  '"'  -> Right ('"',  xs)+  '['  -> Right ('[',  xs)+  ']'  -> Right (']',  xs)+  '0'  -> Right ('\0', xs)+  '^'  -> Right ('^',  xs)+  _    -> errorAt ("unknown escape \\" ++ [e]) xs+escChar stopC (c:xs)+  | c == stopC = errorAt "unexpected close quote" (c:xs)+  | otherwise  = Right (c, xs)+escChar _ [] = Left "unexpected end of input in literal"++-- | A character class.  A leading @^@ negates it, as in POSIX; write+-- @[\\^]@ for a class containing the caret itself.+classLit :: P (Bool, [(Char, Char)])+classLit s0 = case spaces s0 of+  ('[':'^':xs) -> do+    (rs, r) <- loop xs+    if null rs+      then errorAt "empty negated character class" s0+      else Right ((True, rs), r)+  ('[':xs)     -> do+    (rs, r) <- loop xs+    Right ((False, rs), r)+  s            -> errorAt "expected character class" s+  where+    loop (']':r) = Right ([], r)+    loop []      = Left "unterminated character class"+    loop r0      = do+      (c1, r1) <- escChar ']' r0+      case r1 of+        ('-':']':r2) -> pure ([(c1, c1), ('-', '-')], r2)+        ('-':r2) ->+          do (c2, r3) <- escChar ']' r2+             (rs, r4) <- loop r3+             pure ((c1, c2) : rs, r4)+        _ ->+          do (rs, r2) <- loop r1+             pure ((c1, c1) : rs, r2)++actionLit :: P String+actionLit s0 = case spaces s0 of+  ('{':xs) -> go (1 :: Int) ' ' [] xs+  s        -> errorAt "expected a semantic action" s+  where+    go _ _ _ [] = Left "unterminated semantic action: missing '}'"+    go n prev acc s = case s of+      ('{':'-':r) -> do+        (com, r') <- blockComment (1 :: Int) r+        go n '}' (revApp ("{-" ++ com) acc) r'+      ('"':r) -> do+        (str, r') <- literalBody '"' r+        go n '"' (revApp ('"' : str) acc) r'+      ('\'':r) | not (isIdChar prev) -> do+        (ch, r') <- literalBody '\'' r+        go n '\'' (revApp ('\'' : ch) acc) r'+      ('{':r) -> go (n + 1) '{' ('{' : acc) r+      ('}':r) | n == 1    -> Right (reverse acc, r)+              | otherwise -> go (n - 1) '}' ('}' : acc) r+      (c:_) | c `elem` symChars ->+        let (sym, r) = span (`elem` symChars) s+        in if all (== '-') sym && length sym >= 2+             then let (line, r') = span (/= '\n') r+                  in go n '\n' (revApp (sym ++ line) acc) r'+             else go n (last sym) (revApp sym acc) r+      (c:r) -> go n c (c : acc) r++    isIdChar c = c == '_' || c == '\''+              || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')+              || (c >= '0' && c <= '9')++    literalBody _ [] = Left "unterminated literal in a semantic action"+    literalBody q ('\\':c:r)     = do (b, r') <- literalBody q r+                                      Right ('\\' : c : b, r')+    literalBody q (c:r) | c == q = Right ([c], r)+    literalBody q (c:r)          = do (b, r') <- literalBody q r+                                      Right (c : b, r')++    blockComment _ []              = Left "unterminated {- -} comment in a semantic action"+    blockComment k ('-':'}':r)+      | k == 1                     = Right ("-}", r)+      | otherwise                  = do (c, r') <- blockComment (k - 1) r+                                        Right ("-}" ++ c, r')+    blockComment k ('{':'-':r)     = do (c, r') <- blockComment (k + 1) r+                                        Right ("{-" ++ c, r')+    blockComment k (c:r)           = do (c', r') <- blockComment k r+                                        Right (c : c', r')++    revApp xs acc = reverse xs ++ acc++    symChars = "!#$%&*+./<=>?@\\^|-~:"++parseExpr :: P PExpr+parseExpr s0 = do+  (e1, s1) <- parseSeq s0+  loop [e1] s1+  where+    loop acc s = case tok "/" s of+      Right (_, s') -> do (e, s'') <- parseSeq s'+                          loop (e:acc) s''+      Left _        -> case reverse acc of+        [x] -> Right (x, s)+        xs  -> Right (EChoice xs, s)++parseSeq :: P PExpr+parseSeq s0 = loop [] s0+  where+    loop acc s = case parseLabelled s of+      Right (it, s') -> loop (it:acc) s'+      Left _         -> case actionLit s of+        Right (act, s') -> Right (ESeq (reverse acc) (Just act), s')+        Left _          -> Right (ESeq (reverse acc) Nothing,    s)++parseLabelled :: P Item+parseLabelled s = case label s of+  Just (l, s1) -> do (e, s2) <- parsePrefix s1+                     pure (Item (Just l) e, s2)+  Nothing      -> do (e, s1) <- parsePrefix s+                     pure (Item Nothing e, s1)+  where+    label s' = case ident s' of+      Right (name, s1) -> case tok ":" s1 of+        Right (_, s2) -> Just (name, s2)+        Left _        -> Nothing+      Left _ -> Nothing++parsePrefix :: P PExpr+parsePrefix s = case tok "&" s of+  Right (_, s') -> do (e, s'') <- parseSuffix s'; pure (EAnd e, s'')+  Left _        -> case tok "!" s of+    Right (_, s') -> do (e, s'') <- parseSuffix s'; pure (ENot e, s'')+    Left _        -> parseSuffix s++parseSuffix :: P PExpr+parseSuffix s = do+  (p, s1) <- parsePrimary s+  loop p s1+  where+    loop p s1 = case tok "?" s1 of+      Right (_, s2) -> loop (EOpt p) s2+      Left _ -> case tok "*" s1 of+        Right (_, s2) -> loop (EStar p) s2+        Left _ -> case tok "+" s1 of+          Right (_, s2) -> loop (EPlus p) s2+          Left _ -> case indented EIndent "^" p s1 of+            Right (p', s2) -> loop p' s2+            Left _ -> case indented EPos "_" p s1 of+              Right (p', s2) -> loop p' s2+              Left _         -> Right (p, s1)++    indented con marker p s1 = do+      (_, s2) <- tok marker s1+      (r, s3) <- parseRel s2+      pure (con r p, s3)++parseRel :: P RelS+parseRel s = case tok ">=" s of+  Right (_, s1) -> Right (RGe, s1)+  Left _ -> case tok ">" s of+    Right (_, s1) -> Right (RGt, s1)+    Left _ -> case tok "=" s of+      Right (_, s1) -> Right (REq, s1)+      Left _ -> case tok "~" s of+        Right (_, s1) -> Right (RAny, s1)+        Left _ -> case tok "@" s of+          Right (_, s1) -> do (name, s2) <- ident s1+                              pure (RNamed name, s2)+          Left _ -> case tok "+" s of+            Right (_, s1) -> case span isDigit (spaces s1) of+              ([], _)     -> errorAt "expected a number after '+'" s1+              (ds, s2)    -> Right (ROffset (read ds), s2)+            Left _ -> errorAt "expected an indentation relation" s+  where+    isDigit c = c >= '0' && c <= '9'++parsePrimary :: P PExpr+parsePrimary s =+  case tok "(" s of+    Right (_, s1) -> do (e, s2) <- parseExpr s1+                        (_, s3) <- tok ")" s2+                        pure (e, s3)+    Left _ -> case parseAlign s of+     Right r -> Right r+     Left _ -> case tok "." s of+      Right (_, s1) -> Right (EDot, s1)+      Left _ -> case charLit s of+        Right (c, s1) -> Right (EChar c, s1)+        Left _ -> case strLit s of+          Right (cs, s1) -> Right (EString cs, s1)+          Left _ -> case classLit s of+            Right ((neg, rs), s1) -> Right (EClass neg rs, s1)+            Left _ -> case ident s of+              Right (name, s1) ->+                case tok "<-" s1 of+                  Right _  -> errorAt "definition where expression expected" s+                  Left _   -> Right (ENT name, s1)+              Left _ -> errorAt "expected primary expression" s++parseAlign :: P PExpr+parseAlign s = do+  (_, s1) <- tok "|" s+  (e, s2) <- parseExpr s1+  if isEmptyExpr e+    then errorAt "empty alignment: write |e| with a non-empty e" s+    else do (_, s3) <- tok "|" s2+            pure (EAlign e, s3)+  where+    isEmptyExpr (ESeq [] Nothing) = True+    isEmptyExpr _                 = False++parseGrammar :: P [Def]+parseGrammar s0 = loop [] s0+  where+    loop acc s = case ident s of+      Left _ -> case spaces s of+        [] -> Right (reverse acc, "")+        s' -> errorAt "expected definition or end of input" s'+      Right (name, s1) -> do+        (ty, s2) <- resultType s1+        (_, s3)  <- tok "<-" s2+        (e, s4)  <- parseExpr s3+        loop (Def name ty e : acc) s4++    -- @name :: T <- body@.  The annotation runs to the @<-@, which no type+    -- can contain, so it needs no parsing here: it is handed to+    -- "PEG.QQ.HsExp" as text.+    resultType s = case tok "::" s of+      Left _        -> Right (Nothing, s)+      Right (_, s1) -> case breakOnArrow (spaces s1) of+        Nothing        -> errorAt "expected '<-' after a result type" s1+        Just (ty, s2)+          | all isSpaceC ty -> errorAt "empty result type" s1+          | otherwise       -> Right (Just ty, s2)++    breakOnArrow = go []+      where+        go _   []             = Nothing+        go acc r@('<':'-':_)  = Just (reverse acc, r)+        go acc (c:cs)         = go (c:acc) cs++    isSpaceC c = c == ' ' || c == '\t' || c == '\n' || c == '\r'++-- | Consume the @%key value@ lines a grammar may start with.+--+-- Only the header is scanned, and only before the first rule, so a @%@ inside+-- a semantic action is never mistaken for a directive.+parseDirectives :: P [Directive]+parseDirectives = loop []+  where+    loop acc s = case spaces s of+      ('%':rest) ->+        let (key, r1)  = span isKeyChar rest+            (val, r2)  = span (/= '\n') r1+        in if null key+             then errorAt "expected a directive name after '%'" s+             else loop (Directive key (trim val) : acc) r2+      s' -> Right (reverse acc, s')++    isKeyChar c = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')+    trim = dropWhile isSpaceC . reverse . dropWhile isSpaceC . reverse+    isSpaceC c = c == ' ' || c == '\t' || c == '\r'
src/PEG/Syntax.hs view
@@ -12,10 +12,10 @@ -- | The PEG expression GADT and combinator API. -- -- 'PExp' is the core type: a GADT indexed by the input stream, the grammar--- environment, the 'PEG.Type.Ty' of the expression (nullability + FIRST set),--- and the Haskell result type.  Combinators like '<*>.' and '.||.' propagate--- type information at the kind level so that 'PEG.Grammar.Acyclic' can be--- checked without running the parser.+-- 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. -- -- The first parameter, @s@, is the stream the expression consumes; see -- "PEG.Stream".  It appears in the type because a character class produces a@@ -24,10 +24,36 @@ -- -- Most users will not build 'PExp' values directly; instead they use the -- quasi-quoter in "PEG.QQ".+--+-- == What is no longer in the index+--+-- A 'PExp' used to carry a fourth index, its nullability and FIRST set, from+-- which @PEG.Grammar.Acyclic@ derived a type error for a left-recursive+-- grammar.  Both are still computed and left recursion is still rejected, by+-- "PEG.Analysis" when the grammar is spliced rather than by GHC on every+-- compilation that mentions it; "PEG.Type" says what that cost and what it+-- buys, and "PEG.Grammar" says what it gives up.+--+-- One consequence shows up here rather than there.  'Star' used to demand a+-- non-nullable argument, so that @e*@ on an @e@ matching the empty string was+-- a type error; now nothing in the type stops it, and it is "PEG.Analysis"+-- that reports it.  A 'Star' built by hand over a nullable expression will+-- loop at run time.+--+-- The other consequence is that a combinator over expressions is now an+-- ordinary polymorphic function.  What had to be written+--+-- @+-- 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. module PEG.Syntax   ( Name (..)   , PExp (..)   , nt+  , ntw   , sat   , charClass   , notCharClass@@ -46,10 +72,6 @@   , plus   , oneOf   , stringNE-  , SeqTy-  , ChoiceTy-  , NTTy-  , NTGo   ) where  import Data.Kind    (Type)@@ -65,43 +87,6 @@ -- | A singleton witness for a non-terminal name @n@. data Name (n :: Symbol) = Name --- | The 'Ty' of a sequence @e1 e2@.------ Written as a projective type synonym rather than a type family so that it--- reduces to a @'MkTy'@ head even when its operands are still abstract.  That--- is what lets a polymorphic combinator such as------ @--- lexeme :: PExp s env ty a -> PExp s env (SeqTy ty ('MkTy 'True '[])) a--- @------ compose without the caller having to get the nesting of 'SeqTy' exactly--- right.  The cost it used to carry — an exponential blow-up as the operands--- get duplicated across the right-hand side — came from 'Union' and--- 'ConsIfAbsent', not from here; see "PEG.TyLevel".-type SeqTy t1 t2 =-  'MkTy (And (Nullable t1) (Nullable t2))-        (Union (First t1) (If (Nullable t1) (First t2) '[]))---- | The 'Ty' of an ordered choice @e1 \/ e2@.-type ChoiceTy t1 t2 =-  'MkTy (Or  (Nullable t1) (Nullable t2))-        (Union (First t1) (First t2))---- | The 'Ty' of a non-terminal reference @n@ looked up in @env@.-type NTTy n env = NTGo n (TyOf (Lookup n env))---- | The 'Ty' of a reference to a non-terminal named @n@ whose own 'Ty' is--- @t@.------ 'NT' and 'nt' are stated in terms of this rather than 'NTTy' so that the--- environment is searched /once/ per occurrence, by the constructor's--- @Lookup n env ~ 'EnvEntry ty a@ equality.  Naming @Lookup n env@ twice, as--- an expansion of 'NTTy' does, doubles the cost of what profiling shows to be--- the dominant term in checking a large grammar.-type family NTGo (n :: Symbol) (t :: Ty) :: Ty where-  NTGo n ('MkTy nu f) = 'MkTy nu (ConsIfAbsent n f)- -- | A typed PEG expression over the stream @s@. -- -- Constructors correspond to the standard PEG operators:@@ -122,61 +107,85 @@ -- * '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) (ty :: Ty) (a :: Type) where-  Pure     :: a -> PExp s env ('MkTy 'True '[]) a-  Term     :: Char -> PExp s env ('MkTy 'False '[]) Char+data PExp (s :: Type) (env :: Env) (a :: Type) where+  Pure     :: a -> PExp s env a+  Term     :: Char -> PExp s env Char   -- | Match one character of a class.  This is what a character class such as   -- @[a-zA-Z0-9_]@ compiles to: a single bit test instead of a chain of   -- ordered choices.-  Sat      :: !CharSet -> PExp s env ('MkTy 'False '[]) Char-  -- | Match a string literal.  The string must be non-empty (the 'Ty' index-  -- claims the expression is not nullable); use 'pureP' @""@ otherwise.+  Sat      :: !CharSet -> PExp s env 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 ('MkTy 'False '[]) String+  Str      :: String -> PExp s env String   -- | Match the longest run of characters belonging to a class, possibly   -- empty — what @[a-z]*@ compiles to.  The result is a chunk of the input   -- stream, so on 'Data.Text.Text' this is a slice and costs no copy.-  Span     :: !CharSet -> PExp s env ('MkTy 'True  '[]) s+  Span     :: !CharSet -> PExp s env s   -- | As 'Span', but the run must be non-empty: @[a-z]+@.-  Span1    :: !CharSet -> PExp s env ('MkTy 'False '[]) s-  AnyChar  :: PExp s env ('MkTy 'False '[]) Char+  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 variables @ty@ and @a@.  Passing-  -- @TyOf (Lookup n env)@ straight to 'KnownMember' instead makes GHC-  -- re-reduce the lookup at every step of the instance chain that walks-  -- @env@, which costs @O(|env|^2)@ per non-terminal occurrence.-  NT       :: forall n ty s env a.+  -- 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 ty a+              , Lookup n env ~ 'EnvEntry a               , KnownMember n env a               )            => Name n-           -> PExp s env (NTGo n ty) a-  Seq      :: PExp s env t1 (a -> b)-           -> PExp s env t2 a-           -> PExp s env (SeqTy t1 t2) b-  Choice   :: PExp s env t1 a-           -> PExp s env t2 a-           -> PExp s env (ChoiceTy t1 t2) a-  Star     :: PExp s env ('MkTy 'False f) a-           -> PExp s env ('MkTy 'True  f) [a]-  Not      :: PExp s env ('MkTy n f) a-           -> PExp s env ('MkTy 'True f) ()+           -> 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+  -- | 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 ()   Map      :: (a -> b)-           -> PExp s env ty a-           -> PExp s env ty b+           -> PExp s env a+           -> PExp s env b   Indent   :: Rel n-           -> PExp s env ty a-           -> PExp s env ty a+           -> PExp s env a+           -> PExp s env a   Position :: Rel n-           -> PExp s env ty a-           -> PExp s env ty a-  Align    :: PExp s env ty a-           -> PExp s env ty a+           -> PExp s env a+           -> PExp s env a+  Align    :: PExp s env a+           -> PExp s env a -instance Functor (PExp s env ty) where+instance Functor (PExp s env) where   fmap = Map  -- | Reference a non-terminal by name using a type application:@@ -185,102 +194,114 @@ -- The name is deliberately the /first/ quantified variable, so that -- @nt \@\"expr\"@ keeps working: the stream and environment are recovered by -- unification.-nt :: forall n env s ty a.+nt :: forall n env s a.       ( KnownSymbol n-      , Lookup n env ~ 'EnvEntry ty a+      , Lookup n env ~ 'EnvEntry a       , KnownMember n env a       )-   => PExp s env (NTGo n ty) a+   => PExp s env a nt = NT (Name :: Name n) +-- | Reference a non-terminal by name, supplying the membership proof:+-- @ntw \@"ruleName" (There Here)@.+--+-- This is what a generated grammar emits; see 'NTW'.+ntw :: forall n env s a.+       ( KnownSymbol n+       , Lookup n env ~ 'EnvEntry a+       )+    => Member n env a+    -> PExp s env a+ntw = NTW (Name :: Name n)+ -- | Succeed without consuming any input.-pureP :: a -> PExp s env ('MkTy 'True '[]) a+pureP :: a -> PExp s env a pureP = Pure  -- | Apply a function to the result of an expression.-fmapP :: (a -> b) -> PExp s env ty a -> PExp s env ty b+fmapP :: (a -> b) -> PExp s env a -> PExp s env b fmapP = Map  -- | Require the sub-expression to satisfy the given column relation.-indent :: Rel n -> PExp s env ty a -> PExp s env ty a+indent :: Rel n -> PExp s env a -> PExp s env a indent = Indent  -- | Override the token mode for the sub-expression.-position :: Rel n -> PExp s env ty a -> PExp s env ty a+position :: Rel n -> PExp s env a -> PExp s env a position = Position  -- | Require the sub-expression to start at the current alignment column.-align :: PExp s env ty a -> PExp s env ty a+align :: PExp s env a -> PExp s env a align = Align  -- | Infix synonym for 'fmapP'.-(<$>.) :: (a -> b) -> PExp s env ty a -> PExp s env ty b+(<$>.) :: (a -> b) -> PExp s env a -> PExp s env b (<$>.) = Map infixl 4 <$>.  -- | Infix sequential composition.-(<*>.) :: PExp s env t1 (a -> b)-       -> PExp s env t2 a-       -> PExp s env (SeqTy t1 t2) b+(<*>.) :: PExp s env (a -> b)+       -> PExp s env a+       -> PExp s env b (<*>.) = Seq infixl 4 <*>.  -- | Sequence two expressions, discarding the result of the first.-(.>>.) :: PExp s env t1 a-       -> PExp s env t2 b-       -> PExp s env (SeqTy t1 t2) b+(.>>.) :: PExp s env a+       -> PExp s env b+       -> PExp s env 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 t1 a -> PExp s env t2 a -> PExp s env (ChoiceTy t1 t2) a+(.||.) :: PExp s env a -> PExp s env a -> PExp s env a (.||.) = Choice infixl 5 .||.  -- | Optional match: @opt e = (Just \<$\>. e) .||. pureP Nothing@.-opt :: PExp s env t a-    -> PExp s env (ChoiceTy t ('MkTy 'True '[])) (Maybe a)+opt :: PExp s env a -> PExp s env (Maybe a) opt e = (Just <$>. e) .||. pureP Nothing  -- | One-or-more: @plus e = (:) \<$\>. e \<*\>. Star e@. --+-- As for 'Star', @e@ must not match the empty string.+-- -- For a single character class, prefer 'spanOf1': it matches the whole run in -- one scan and returns a chunk of the stream instead of a list.-plus :: PExp s env ('MkTy 'False f) a-     -> PExp s env (SeqTy ('MkTy 'False f) ('MkTy 'True f)) [a]+plus :: PExp s env a -> PExp s env [a] plus e = (:) <$>. e <*>. Star e  -- | Match any character of the given set.-sat :: CharSet -> PExp s env ('MkTy 'False '[]) Char+sat :: CharSet -> PExp s env Char sat = Sat  -- | Match any character inside one of the given inclusive ranges.--- This is the representation the quasi-quoter emits for @[a-z0-9]@ and+-- This is the representation the quasi-quoter emits for @[a-z]@ and -- friends.-charClass :: [(Char, Char)] -> PExp s env ('MkTy 'False '[]) Char+charClass :: [(Char, Char)] -> PExp s env Char charClass = Sat . CS.fromRanges  -- | Match any character /outside/ the given inclusive ranges. -- The quasi-quoter emits this for @[^\"]@.-notCharClass :: [(Char, Char)] -> PExp s env ('MkTy 'False '[]) Char+notCharClass :: [(Char, Char)] -> PExp s env Char notCharClass = Sat . CS.notInRanges  -- | Match the longest run of characters of the set, possibly empty.  The -- result is a chunk of the input stream.-spanOf :: CharSet -> PExp s env ('MkTy 'True '[]) s+spanOf :: CharSet -> PExp s env s spanOf = Span  -- | Match a non-empty run of characters of the set.-spanOf1 :: CharSet -> PExp s env ('MkTy 'False '[]) s+spanOf1 :: CharSet -> PExp s env s spanOf1 = Span1  -- | Match any character in the given list. The list must be non-empty.-oneOf :: [Char] -> PExp s env ('MkTy 'False '[]) Char+oneOf :: [Char] -> PExp s env 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 ('MkTy 'False '[]) String+stringNE :: String -> PExp s env String stringNE [] = error "PEG.Syntax.stringNE: empty string" stringNE s  = Str s
src/PEG/TyLevel.hs view
@@ -4,134 +4,46 @@ {-# LANGUAGE TypeOperators        #-} {-# LANGUAGE UndecidableInstances #-} --- | Type-level utilities: boolean logic, symbol equality, set operations,--- and environment lookup.------ These type families are used internally to compute the FIRST sets and--- nullability of PEG expressions at the kind level, enabling the--- 'PEG.Grammar.Acyclic' constraint to be resolved at compile time.------ == Representation of FIRST sets------ A FIRST set is a @['Symbol']@ kept /strictly sorted/ by 'CmpSymbol'.--- Sortedness is the whole point: it makes the representation canonical (one--- set, one type), so 'Union' is a single-pass merge and 'Elem' can stop at--- the first symbol greater than the one it is looking for.------ == Why the families are written this way+-- | Looking a non-terminal up in the grammar environment. ----- Every clause below mentions each of its arguments — and in particular each--- recursive call — /exactly once/ on the right-hand side.  This is not a--- style choice.  A clause such as+-- This is the only type-level computation the library still does, and it is+-- the hot path: there is one lookup per occurrence of every non-terminal in+-- the grammar, so it is written to do as little as possible per entry+-- scanned. ----- @--- ConsIfAbsent x xs = If (Elem x xs) xs (x ': xs)   -- DON'T--- @+-- == What used to be here ----- mentions @xs@ three times, and @xs@ is normally an unreduced application--- of 'Union'.  GHC therefore has three copies of the pending computation to--- reduce, each of which triples again one level down: a union of two sets of--- size @n@ costs @3^n@ reductions rather than @n@.  Dispatching on an--- already-computed 'Ordering' in a separate family keeps every right-hand--- side linear in its arguments.+-- Sorted-set machinery — 'Union', membership, insertion — over the FIRST sets+-- that environment entries used to carry, together with the boolean families+-- that combined their nullability.  Those sets now live in "PEG.Analysis",+-- which computes them at splice time; see "PEG.Type" for why they left the+-- types.  What remains is the search, and with the sets gone it is a search+-- over an environment that is linear in the size of the grammar rather than+-- quadratic. module PEG.TyLevel-  ( If-  , And-  , Or-  , SymEq-  , Elem-  , Union-  , ConsIfAbsent-  , Lookup+  ( Lookup   , Names   ) where -import GHC.TypeLits (CmpSymbol, ErrorMessage (..), Symbol, TypeError)+import GHC.TypeLits (ErrorMessage (..), Symbol, TypeError)  import PEG.Type -type family If (c :: Bool) (t :: k) (e :: k) :: k where-  If 'True  t _ = t-  If 'False _ e = e--type family And (a :: Bool) (b :: Bool) :: Bool where-  And 'True  b = b-  And 'False _ = 'False--type family Or (a :: Bool) (b :: Bool) :: Bool where-  Or 'True  _ = 'True-  Or 'False b = b--type family SymEq (a :: Symbol) (b :: Symbol) :: Bool where-  SymEq a b = IsEQ (CmpSymbol a b)--type family IsEQ (o :: Ordering) :: Bool where-  IsEQ 'EQ = 'True-  IsEQ _   = 'False---- | Is @x@ a member of the sorted set @xs@?------ Stops as soon as it reaches a symbol greater than @x@, so a miss costs--- half a scan on average rather than a full one.-type family Elem (x :: Symbol) (xs :: [Symbol]) :: Bool where-  Elem _ '[]       = 'False-  Elem x (y ': ys) = ElemGo (CmpSymbol x y) x ys--type family ElemGo (o :: Ordering) (x :: Symbol) (ys :: [Symbol]) :: Bool where-  ElemGo 'EQ _ _  = 'True-  ElemGo 'LT _ _  = 'False-  ElemGo 'GT x ys = Elem x ys---- | Insert @x@ into the sorted set @xs@, keeping it sorted and duplicate-free.-type family ConsIfAbsent (x :: Symbol) (xs :: [Symbol]) :: [Symbol] where-  ConsIfAbsent x '[]       = '[x]-  ConsIfAbsent x (y ': ys) = InsGo (CmpSymbol x y) x y ys--type family InsGo (o :: Ordering) (x :: Symbol) (y :: Symbol)-                  (ys :: [Symbol]) :: [Symbol] where-  InsGo 'LT x y ys = x ': y ': ys-  InsGo 'EQ _ y ys = y ': ys-  InsGo 'GT x y ys = y ': ConsIfAbsent x ys---- | Union of two sorted sets: a single merge pass, @O(|xs| + |ys|)@.------ The merge nests one type-family reduction per element of the result, so a--- FIRST set of more than about a hundred non-terminals runs into GHC's--- default reduction limit and reports @Reduction stack overflow@.  That is a--- limit, not a slowdown: @-freduction-depth=0@ lifts it, and a union of two--- 128-element sets then takes about 0.3 s.-type family Union (xs :: [Symbol]) (ys :: [Symbol]) :: [Symbol] where-  Union '[]       ys        = ys-  Union (x ': xs) '[]       = x ': xs-  Union (x ': xs) (y ': ys) = MergeGo (CmpSymbol x y) x xs y ys--type family MergeGo (o :: Ordering) (x :: Symbol) (xs :: [Symbol])-                    (y :: Symbol) (ys :: [Symbol]) :: [Symbol] where-  MergeGo 'LT x xs y ys = x ': Union xs (y ': ys)-  MergeGo 'EQ x xs _ ys = x ': Union xs ys-  MergeGo 'GT x xs y ys = y ': Union (x ': xs) ys- -- | Look up a non-terminal's entry in the environment. ----- This is the hot path: there is one lookup per occurrence of every--- non-terminal in the grammar, so it is written to do as little as possible--- per entry scanned.--- -- Two things matter.  The search proper ('LookupMb') carries only the tail it -- still has to scan — threading the /whole/ environment through it so the -- not-found case could name the available non-terminals costs a traversal of--- that environment at every step, and an environment of @n@ rules is itself--- @O(n^2)@ type nodes because every rule carries a FIRST set.  The--- environment is therefore named once, in 'Found', which only reduces after--- the search has finished.+-- that environment at every step.  The environment is therefore named once,+-- in 'Found', which only reduces after the search has finished; measured+-- against a variant that does not name it at all, the good error message+-- costs about 5%. -- -- And the match is on a /non-linear/ pattern — @s@ appears twice in the -- second clause — rather than on @CmpSymbol s t@ dispatched through a helper -- family.  GHC decides the clause by syntactic equality and by apartness for -- the fall-through, which is one type-family reduction per entry instead of--- two.  (The trick is @Data.Type.Map@'s, from @type-level-sets@.)  It costs--- nothing here: unlike 'Elem', this search has no sortedness to exploit, so--- there was never a third case to short-circuit on.+-- two.  (The trick is @Data.Type.Map@'s, from @type-level-sets@.) type family Lookup (s :: Symbol) (env :: Env) :: EnvEntry where   Lookup s env = Found s env (LookupMb s env) @@ -147,6 +59,7 @@     TypeError ('Text "Undefined non-terminal: " ':<>: 'ShowType s          ':$$: 'Text "Available non-terminals: " ':<>: 'ShowType (Names env)) +-- | The names an environment defines, for the message above. type family Names (env :: Env) :: [Symbol] where   Names '[]               = '[]   Names ('(s, _) ': rest) = s ': Names rest
src/PEG/Type.hs view
@@ -3,52 +3,55 @@ {-# LANGUAGE TypeFamilies   #-} {-# LANGUAGE TypeOperators  #-} --- | Type-level representation of PEG type information.+-- | The grammar environment: what a non-terminal's name is bound to. ----- Each non-terminal carries a 'Ty': a pair of its /nullability/--- (can it match the empty string?) and its /FIRST set/ (which non-terminal--- names can appear at the head of a derivation?).--- Both pieces of information are tracked as type-level data and used by the--- 'PEG.Grammar.Acyclic' constraint to reject left-recursive grammars at--- compile time.+-- An environment maps each non-terminal's name to the Haskell type its rule+-- returns, and to nothing else.  A reference to a non-terminal is checked+-- against it — @nt \@\"expr\"@ is a type error unless @expr@ is a rule, and it+-- has whatever type @expr@'s rule has — which is the whole of what the+-- environment is for.+--+-- == What used to be here+--+-- Entries used to carry a 'Ty' as well: the rule's nullability and its FIRST+-- set, the set of non-terminals that can begin a derivation of it.  That is+-- what made left recursion a type error, by way of a @PEG.Grammar.Acyclic@+-- constraint that checked no rule was in its own FIRST set.+--+-- It was also, measurably, the whole cost of compiling a large grammar.  A+-- FIRST set grows with the grammar, so an environment of @n@ rules was+-- @O(n^2)@ type nodes, and each of the @2n@ reference constraints in the+-- rules had to be solved against it: 64 rules cost GHC 15 s, and the same+-- environment with a payload nothing reads at all was 15x an environment+-- without one.  Not reducing the FIRST-set arithmetic was worth nothing by+-- comparison — it was never the arithmetic, only the size.  See+-- @bench-compile/@.+--+-- Nullability and FIRST sets are still computed, and left recursion is still+-- rejected before a parser can be built from a left-recursive grammar — by+-- "PEG.Analysis", at splice time, once, in milliseconds, with the offending+-- rule and its cycle named.  What changed is that GHC no longer recomputes+-- them on every compilation of every module that mentions the grammar.  The+-- cost of that trade is real and is stated in "PEG.Grammar": a 'Rules' value+-- assembled by hand, without going through a quasi-quoter, is no longer+-- checked for left recursion by anything. module PEG.Type-  ( Ty (..)-  , Nullable-  , First-  , EnvEntry (..)+  ( EnvEntry (..)   , Env-  , TyOf   , ResOf   ) where  import Data.Kind    (Type) import GHC.TypeLits (Symbol) --- | A PEG type: nullability flag and FIRST set.------ @'MkTy' n fs@ means the expression may match the empty string iff @n ~ 'True@,--- and the set of non-terminal names that can begin a derivation is @fs@.-data Ty = MkTy Bool [Symbol]---- | Extract the nullability flag from a 'Ty'.-type family Nullable (t :: Ty) :: Bool where-  Nullable ('MkTy n _) = n---- | Extract the FIRST set (list of non-terminal names) from a 'Ty'.-type family First (t :: Ty) :: [Symbol] where-  First ('MkTy _ f) = f---- | An entry in the grammar environment: a 'Ty' paired with its result type.-data EnvEntry = EnvEntry Ty Type+-- | An entry in the grammar environment: the type a rule's semantic action+-- produces.+data EnvEntry = EnvEntry Type  -- | A grammar environment: a type-level association list mapping non-terminal -- names ('Symbol') to their 'EnvEntry'. type Env = [(Symbol, EnvEntry)] --- | Extract the 'Ty' from an 'EnvEntry'.-type family TyOf (e :: EnvEntry) :: Ty where-  TyOf ('EnvEntry t _) = t- -- | Extract the result type from an 'EnvEntry'. type family ResOf (e :: EnvEntry) :: Type where-  ResOf ('EnvEntry _ a) = a+  ResOf ('EnvEntry a) = a
+ tests/Analysis.hs view
@@ -0,0 +1,483 @@+{-# LANGUAGE DataKinds        #-}+{-# LANGUAGE QuasiQuotes      #-}+{-# LANGUAGE TemplateHaskell  #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators    #-}+-- | Checks "PEG.Analysis" against what its results are supposed to mean.+--+-- == Why this is the whole guarantee now+--+-- It did not use to be.  Environment entries carried their FIRST sets as+-- type-level data and @Acyclic@ checked them, so every grammar that compiled+-- was a grammar GHC had agreed with, and this module only had to cover the+-- cases GHC could not see.  Entries no longer carry them — "PEG.Type" says+-- why — so nothing recomputes what "PEG.Analysis" concludes.  A left-recursive+-- grammar it accepts is a parser that loops.+--+-- So the analysis is checked here against a separate statement of what its+-- two results mean, written to be obvious rather than fast:+--+-- * a rule is nullable iff it is in the least fixpoint of "can match the+--   empty string";+-- * @FIRST(r)@ is the set of non-terminals reachable from @r@ under the+--   one-step head relation — @m@ is a head of @r@ when @m@ can be the first+--   non-terminal a derivation of @r@ reaches without any other non-terminal+--   being entered first.+--+-- The second is the definition left recursion is stated in terms of: @r@ is+-- left-recursive exactly when @r@ is reachable from itself.  "PEG.Analysis"+-- computes it a different way — it propagates whole FIRST sets through+-- 'PEG.Analysis.seqTy' and 'PEG.Analysis.choiceTy' as it iterates, over+-- sorted sets merged pairwise — so the two agreeing is worth something.+--+-- They are compared over every grammar in @examples/@ and over a few hundred+-- generated ones, which is what covers the shapes the examples happen not to+-- have.+--+-- What is also checked here:+--+-- * The diagnostics, which no example can exercise, because an example that+--   triggered one would not compile.+-- * The /shape/ of what 'PEG.QQ.pegGrammar' generates, against a literal+--   written out below, and that the grammar it generates parses.+--+-- Result types are not compared against the analysis — it does not compute+-- them, and cannot: they come from the Haskell semantic actions, which GHC+-- types long after the splice has run.+module Main (main) where++import Control.Monad (forM, unless)+import Data.List     (isPrefixOf, nub, sort, union)+import System.Exit   (exitFailure)++import Data.Proxy    (Proxy (..))++import PEG+import PEG.QQ        (pegGrammar)+import PEG.Analysis  (Diagnostic (..), Ty (..), analyse, renderDiagnostic)+import PEG.QQ.Syntax (Def (..), Item (..), PExpr (..), parseDirectives,+                      parseGrammar)++exampleFiles :: [FilePath]+exampleFiles =+  [ "examples/Arith.hs"+  , "examples/Layout.hs"+  , "examples/Patterns.hs"+  , "examples/Compat.hs"+  ]++main :: IO ()+main = do+  results <- concat <$> mapM checkFile exampleFiles+  let failures = [ msg | Left msg <- results ]+      checked  = length [ () | Right () <- results ]+  mapM_ putStrLn failures+  putStrLn ("PEG.Analysis: " ++ show checked ++ " grammars in examples/ agree\+            \ with the specification")+  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/, \+             \found fewer"+    exitFailure+  let generated = [ checkDefs ("generated/" ++ show i) g+                  | (i, g) <- zip [0 :: Int ..] generatedGrammars ]+      genBad    = [ msg | Left msg <- generated ]+  mapM_ putStrLn (take 5 genBad)+  putStrLn ("PEG.Analysis: " ++ show (length generated - length genBad)+              ++ " of " ++ show (length generated)+              ++ " generated grammars agree with the specification"+              ++ " (" ++ show recursive ++ " left-recursive rules, "+              ++ show withHeads ++ " with a non-empty FIRST set)")+  unless (null genBad) exitFailure+  -- Agreement is easy to reach vacuously: a generator that stopped emitting+  -- references would make every FIRST set empty and every grammar pass.  The+  -- corpus has to keep containing both answers.+  unless (recursive >= 20 && withHeads >= 20) $ do+    putStrLn "PEG.Analysis: the generated corpus has gone degenerate"+    exitFailure+  let checks = standaloneChecks ++ [witnessCheck] ++ generatedChecks+  mapM_ report checks+  unless (all (\(_, ok) -> ok) checks) exitFailure+  where+    report (name, ok) =+      putStrLn ((if ok then "ok   " else "FAIL ") ++ name)++    -- Coverage of the generated corpus, measured through the specification+    -- rather than the analysis, so that it says what the corpus contains and+    -- not what the code under test thinks it contains.+    recursive = length [ () | g <- generatedGrammars+                            , (n, Ty _ f) <- specEnv g, n `elem` f ]+    withHeads = length [ () | g <- generatedGrammars+                            , any (\(_, Ty _ f) -> not (null f)) (specEnv g) ]++--------------------------------------------------------------------------------+-- The specification: what nullability and a FIRST set mean+--------------------------------------------------------------------------------++-- | The least fixpoint of "can match the empty string", over the rules.+--+-- Iterated over the whole system from "nothing is nullable" until it stops+-- changing, which is the definition rather than a way of computing it+-- quickly.+specNullable :: [Def] -> [(String, Bool)]+specNullable defs = fix [ (n, False) | Def n _ _ <- defs ]+  where+    fix m = let m' = [ (n, nu m e) | Def n _ e <- defs ]+            in if m' == m then m else fix m'++    nu m = go+      where+        go (EChar _)      = False+        go EDot           = False+        go (EClass _ _)   = False+        go (EString s)    = null s+        go (ENT n)        = maybe False id (lookup n m)+        go (EAnd _)       = True          -- a lookahead consumes nothing+        go (ENot _)       = True+        go (EOpt _)       = True+        go (EStar _)      = True+        go (EPlus e)      = go e+        go (EIndent _ e)  = go e+        go (EPos _ e)     = go e+        go (EAlign e)     = go e+        go (EChoice es)   = any go es+        go (ESeq its _)   = all (\(Item _ e) -> go e) its++-- | The one-step head relation: the non-terminals that a derivation of this+-- expression can reach first, without entering any other non-terminal on the+-- way.+--+-- A sequence contributes the heads of its first item, and those of the second+-- as well when the first can match the empty string, and so on.+specHeads :: [(String, Bool)] -> PExpr -> [String]+specHeads nulls = go+  where+    nullableOf = specNullableOf nulls++    go (EChar _)     = []+    go EDot          = []+    go (EClass _ _)  = []+    go (EString _)   = []+    go (ENT n)       = [n]+    go (EAnd e)      = go e+    go (ENot e)      = go e+    go (EOpt e)      = go e+    go (EStar e)     = go e+    go (EPlus e)     = go e+    go (EIndent _ e) = go e+    go (EPos _ e)    = go e+    go (EAlign e)    = go e+    go (EChoice es)  = foldl' union [] (map go es)+    go (ESeq its _)  = seqHeads [ e | Item _ e <- its ]++    seqHeads []     = []+    seqHeads (e:es) | nullableOf e = go e `union` seqHeads es+                    | otherwise    = go e++-- | Whether an expression is nullable, given the rules' nullability.+specNullableOf :: [(String, Bool)] -> PExpr -> Bool+specNullableOf nulls = go+  where+    go (EChar _)     = False+    go EDot          = False+    go (EClass _ _)  = False+    go (EString s)   = null s+    go (ENT n)       = maybe False id (lookup n nulls)+    go (EAnd _)      = True+    go (ENot _)      = True+    go (EOpt _)      = True+    go (EStar _)     = True+    go (EPlus e)     = go e+    go (EIndent _ e) = go e+    go (EPos _ e)    = go e+    go (EAlign e)    = go e+    go (EChoice es)  = any go es+    go (ESeq its _)  = all (\(Item _ e) -> go e) its++-- | The environment the analysis is supposed to produce: nullability as+-- above, and each rule's FIRST set as everything reachable from it under+-- 'specHeads'.+specEnv :: [Def] -> [(String, Ty)]+specEnv defs =+  [ (n, Ty (specNullableOf nulls e) (sort (reach (heads e)))) | Def n _ e <- defs ]+  where+    nulls = specNullable defs+    heads = specHeads nulls++    bodyOf n = case [ e | Def m _ e <- defs, m == n ] of+                 (e:_) -> Just e+                 []    -> Nothing++    -- Transitive closure by worklist.  A name the grammar does not define+    -- contributes itself and nothing further, which is how the analysis+    -- treats it too.+    reach = grow []+      where+        grow seen []     = seen+        grow seen (x:xs)+          | x `elem` seen = grow seen xs+          | otherwise     = grow (x : seen)+                                 (maybe [] heads (bodyOf x) ++ xs)++--------------------------------------------------------------------------------+-- Comparing the analysis against it+--------------------------------------------------------------------------------++-- | Check one grammar, whatever the analysis makes of it.+--+-- The two must agree on left recursion — the analysis reports it exactly when+-- a rule is reachable from itself — and, when there is none, on the whole+-- environment.+checkDefs :: String -> [Def] -> Either String ()+checkDefs what defs+  | not (null dups) = Right ()   -- a duplicate rule makes 'specEnv' meaningless+  | otherwise = case analyse defs of+      Left ds+        | not (null [ () | LeftRecursive _ _ <- ds ]) ->+            if null selfReaching+              then Left (what ++ ": analyse reports left recursion, the \+                                 \specification finds no cycle")+              else Right ()+        | otherwise -> Right ()  -- other diagnostics are checked separately+      Right env+        | not (null selfReaching) ->+            Left (what ++ ": analyse accepted a grammar whose rules "+                    ++ show selfReaching ++ " reach themselves")+        | normalise env == normalise spec -> Right ()+        | otherwise -> Left (unlines+            ([ what ++ ": the analysis and the specification disagree" ]+             ++ [ "  " ++ n ++ ": analysed " ++ show got+                          ++ ", specified " ++ show want+                | (n, got) <- normalise env+                , Just want <- [lookup n (normalise spec)]+                , got /= want ]))+  where+    spec  = specEnv defs+    names = [ n | Def n _ _ <- defs ]+    dups  = [ n | n <- nub names, length (filter (== n) names) > 1 ]+    selfReaching = [ n | (n, Ty _ f) <- spec, n `elem` f ]++    -- Compared as plain pairs: the analysis keeps its sets sorted and the+    -- specification builds them with 'union', so ordering is not the claim.+    normalise :: [(String, Ty)] -> [(String, (Bool, [String]))]+    normalise = sort . map (\(n, Ty nu f) -> (n, (nu, sort (nub f))))++checkFile :: FilePath -> IO [Either String ()]+checkFile path = do+  src <- readFile path+  let blocks = [ (b, False) | b <- extractBlocks "[pegRules|" src ]+                 ++ [ (b, True) | b <- extractBlocks "[pegGrammar|" src ]+  forM (zip [1 :: Int ..] blocks) $ \(i, (block, hasDirectives)) ->+    pure $ do+      body <- if hasDirectives+                then fmap snd (left ("directives: " ++) (parseDirectives block))+                else Right block+      (defs, _) <- left ("parse error: " ++) (parseGrammar body)+      left ((path ++ " (" ++ show i ++ "): ") ++) (checkDefs path defs)+  where+    left f = either (Left . f) Right++--------------------------------------------------------------------------------+-- Generated grammars, to cover the shapes the examples happen not to have+--------------------------------------------------------------------------------++-- | A few hundred small grammars, built deterministically so a failure can be+-- reproduced by index.+--+-- The shapes are chosen to make heads interesting: nullable prefixes, so that+-- a sequence's second item contributes; optionals and stars, which are+-- nullable but keep their operand's heads; and references both forwards and+-- backwards, so that some of these are left-recursive and some are not.+generatedGrammars :: [[Def]]+generatedGrammars = [ grammarFrom seed | seed <- take 400 seeds ]+  where+    seeds = iterate (\x -> (x * 1103515245 + 12345) `mod` 2147483648) 1++grammarFrom :: Int -> [Def]+grammarFrom seed0 = snd (foldl' rule (seed0, []) [0 .. n - 1])+  where+    n     = 2 + seed0 `mod` 4+    names = [ "r" ++ show i | i <- [0 .. n - 1] ]++    rule (seed, acc) i =+      let (e, seed') = expr seed 2+      in (seed', acc ++ [Def (names !! i) Nothing e])++    next seed = (seed `div` 65536 `mod` 32768, (seed * 1103515245 + 12345)+                                                 `mod` 2147483648)++    -- A term, at the given remaining depth.  At depth zero only leaves.+    expr seed depth =+      let (k, seed') = next seed+      in case (if depth <= (0 :: Int) then k `mod` 3 else k `mod` 9) of+           0 -> (EChar 'x', seed')+           1 -> (EClass False [('a', 'z')], seed')+           2 -> (ENT (names !! (k `mod` n)), seed')+           3 -> let (e, s') = expr seed' (depth - 1) in (EOpt e, s')+           4 -> let (e, s') = expr seed' (depth - 1) in (EStar e, s')+           5 -> let (e, s') = expr seed' (depth - 1) in (ENot e, s')+           6 -> let (a, s1) = expr seed' (depth - 1)+                    (b, s2) = expr s1 (depth - 1)+                in (EChoice [a, b], s2)+           7 -> let (a, s1) = expr seed' (depth - 1)+                    (b, s2) = expr s1 (depth - 1)+                in (ESeq [Item Nothing a, Item Nothing b] Nothing, s2)+           _ -> let (a, s1) = expr seed' (depth - 1)+                in (ESeq [Item Nothing (EOpt a)+                         , Item Nothing (ENT (names !! (k `mod` n)))]+                         Nothing, s1)++--------------------------------------------------------------------------------+-- Standalone cases: the diagnostics, which no example can exercise because an+-- example that triggered one would not compile.+--------------------------------------------------------------------------------++standaloneChecks :: [(String, Bool)]+standaloneChecks =+  [ ("left recursion is reported with its cycle",+      case run "expr <- e:expr '+' t:term / t:term\nterm <- ds:[0-9]+" of+        Left [LeftRecursive "expr" path] -> path == ["expr", "expr"]+        _                                -> False)+  , ("indirect left recursion reports one cycle, not one per rule",+      case run "a <- x:b\nb <- y:c\nc <- z:a" of+        Left [LeftRecursive n path] -> n `elem` ["a", "b", "c"]+                                         && length path == 4+                                         && take 1 path == take 1 (reverse path)+        _                           -> False)+  , ("a nullable repetition is reported",+      case run "a <- xs:b*\nb <- c:'x'?" of+        Left [NullableStar "a"] -> True+        _                       -> False)+  , ("an undefined non-terminal is reported",+      case run "a <- x:missing" of+        Left [UndefinedNT "missing" ["a"]] -> True+        _                                  -> False)+  , ("a duplicate rule is reported",+      case run "a <- 'x'\na <- 'y'" of+        Left [DuplicateRule "a"] -> True+        _                        -> False)+  , ("a right-recursive grammar is accepted",+      case run "a <- 'x' r:a / 'y'" of+        Right env -> lookup "a" env == Just (Ty False [])+        _         -> False)+  , ("a class repetition is a Span, not a Star",+      case run "a <- xs:[a-z]*" of+        Right env -> lookup "a" env == Just (Ty True [])+        _         -> False)+  , ("a nullable head propagates the next item's FIRST set",+      case run "a <- w:ws n:b\nws <- [ ]*\nb <- 'x'" of+        Right env -> lookup "a" env == Just (Ty False ["b", "ws"])+        _         -> False)+  , ("the reported cycle names every rule on it",+      case run "a <- x:b\nb <- y:c\nc <- z:a" of+        Left [LeftRecursive _ path] -> sort (nub path) == ["a", "b", "c"]+        _                           -> False)+  , ("renderDiagnostic says which rule",+      case run "expr <- e:expr '+' t:term / t:term\nterm <- ds:[0-9]+" of+        Left [d] -> "expr" `isInfix` renderDiagnostic d+        _        -> False)+  ]+  where+    run src = case parseGrammar src of+      Left err        -> Left [UndefinedNT ("parse error: " ++ err) []]+      Right (defs, _) -> analyse defs+    isInfix needle hay = any (needle `isPrefixOf`) (suffixes hay)+    suffixes xs = xs : case xs of { [] -> []; (_:r) -> suffixes r }++--------------------------------------------------------------------------------+-- 'ntw': a reference that carries its own membership proof+--------------------------------------------------------------------------------++-- The environment's order is the rule chain's order, which is what makes+-- @There Here@ name @digits@.  A witness that named the wrong rule would not+-- compile: 'NTW' keeps the @Lookup@ equality that ties the two together.+type NtwEnv =+  '[ '("pair"  , 'EnvEntry (Int, Int))+   , '("digits", 'EnvEntry Int)+   ]++digitsCount :: PExp String NtwEnv Int+digitsCount = fmapP (length . chunkToString) (spanOf1 (fromRanges [('0', '9')]))++ntwGrammar :: Grammar String NtwEnv (Int, Int)+ntwGrammar =+  Grammar+    (RCons (Name @"pair")+           ((,) <$>. ntw @"digits" (There Here)+                <*>. (Term ',' .>>. ntw @"digits" (There Here)))+       (RCons (Name @"digits") digitsCount RNil))+    (ntw @"pair" Here)++--------------------------------------------------------------------------------+-- What pegGrammar generates+--------------------------------------------------------------------------------++[pegGrammar|+  %name  tiny+  %env   TinyEnv+  %start pair++  pair   :: (Int, Int) <- a:digits ',' b:digits+  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)+   ]++sameEnv :: forall (a :: Env) (b :: Env). (a ~ b) => Proxy a -> Proxy b -> ()+sameEnv _ _ = ()++generatedEnvIsExpected :: ()+generatedEnvIsExpected =+  sameEnv (Proxy :: Proxy (TinyEnv String))+          (Proxy :: Proxy (ExpectedTinyEnv String))++generatedChecks :: [(String, Bool)]+generatedChecks =+  [ ("pegGrammar generates the expected environment",+      generatedEnvIsExpected == ())+  , ("a generated grammar parses",+      case parse tiny "12,345" of+        OK r _ rest -> r == (2, 3) && rest == ""+        Fail        -> False)+  ]++witnessCheck :: (String, Bool)+witnessCheck =+  ( "ntw parses through the witness it was given"+  , case parse ntwGrammar "12,345" of+      OK r _ rest -> r == (2, 3) && rest == ""+      Fail        -> False )++--------------------------------------------------------------------------------+-- Extracting the grammars from an example's source+--------------------------------------------------------------------------------++-- | Every @[pegRules| ... |]@ (or @[pegGrammar| ... |]@) block, in order of+-- appearance.+extractBlocks :: String -> String -> [String]+extractBlocks open = go+  where+    go s = case breakOn open s of+      Nothing   -> []+      Just rest -> let (body, after) = breakClose rest in body : go after+    breakClose s = case breakOn "|]" s of+      Nothing   -> (s, "")+      Just rest -> (take (length s - length rest - 2) s, rest)++-- | The input just past the first occurrence of the needle, if any.+breakOn :: String -> String -> Maybe String+breakOn needle = go+  where+    go [] = Nothing+    go s@(_:cs)+      | needle `isPrefixOf` s = Just (drop (length needle) s)+      | otherwise             = go cs
typed-peg.cabal view
@@ -1,12 +1,14 @@ cabal-version:      3.0 name:               typed-peg-version:            0.2.0.0+version:            0.3.0.0 synopsis:           Type-safe PEG parser combinators description:   A library for building Parsing Expression Grammars parsers-  with compile-time safety guarantees. Grammar non-terminals are-  indexed by their nullability and FIRST sets at the type level,-  making left-recursive grammars a type error.+  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.   .   A quasi-quoter (@PEG.QQ@) allows writing grammars in a concrete   DSL syntax. Indentation-sensitive parsing is supported natively@@ -59,6 +61,7 @@   hs-source-dirs:  src   exposed-modules:     PEG+    PEG.Analysis     PEG.CharSet     PEG.Grammar     PEG.Indent@@ -66,6 +69,7 @@     PEG.Parse     PEG.QQ     PEG.QQ.HsExp+    PEG.QQ.Syntax     PEG.Semantics.Simple     PEG.Stream     PEG.Syntax@@ -87,6 +91,16 @@       base     , bytestring     , text+    , typed-peg++test-suite typed-peg-analysis+  import:          common-opts+  type:            exitcode-stdio-1.0+  hs-source-dirs:  tests+  main-is:         Analysis.hs+  other-extensions: TemplateHaskell, QuasiQuotes+  build-depends:+      base     , typed-peg  benchmark typed-peg-bench