packages feed

typed-peg-0.3.0.0: src/PEG/Grammar.hs

{-# LANGUAGE DataKinds            #-}
{-# LANGUAGE FlexibleContexts     #-}
{-# LANGUAGE GADTs                #-}
{-# LANGUAGE KindSignatures       #-}
{-# LANGUAGE TypeFamilies         #-}
{-# LANGUAGE TypeOperators        #-}
{-# LANGUAGE UndecidableInstances #-}

-- | Grammar and rule-set types.
--
-- A 'Grammar' bundles a set of named rules ('Rules') and a start expression.
--
-- == 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 (..)
  ) where

import Data.Kind    (Type)

import PEG.Syntax  (Name, PExp)
import PEG.Type

-- | A typed, heterogeneous list of named grammar rules.
--
-- @'Rules' s env defs@ is a list of rules over the stream @s@ whose bodies
-- reference non-terminals in @env@ and whose definitions together form
-- @defs@.
data Rules (s :: Type) (env :: Env) (defs :: Env) where
  RNil  :: Rules s env '[]
  RCons :: Name n
        -> PExp s env a
        -> Rules s env rest
        -> Rules s env ('(n, 'EnvEntry a) ': rest)

-- | A complete PEG grammar over the stream @s@: a set of mutually recursive
-- rules and a start expression.
--
-- The @Rules s env env@ field is what ties the two halves together: every
-- rule's body is checked against the same environment the rule set defines,
-- so a reference can only name a rule that exists and only at the type that
-- rule has.
--
-- A 'Grammar' is monomorphic in its stream.  To reuse one grammar across
-- several stream types, give it a signature of the form
-- @forall s. 'PEG.Stream.Stream' s => Grammar s Env a@ — but note that
-- doing so turns the value into a function of a dictionary, so the compiled
-- parser is no longer shared between calls.  Prefer a monomorphic top-level
-- signature.
data Grammar (s :: Type) (env :: Env) (a :: Type) where
  Grammar :: Rules s env env
          -> PExp s env a
          -> Grammar s env a