packages feed

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

{-# LANGUAGE DataKinds            #-}
{-# LANGUAGE PolyKinds            #-}
{-# LANGUAGE TypeFamilies         #-}
{-# LANGUAGE TypeOperators        #-}
{-# LANGUAGE UndecidableInstances #-}

-- | Looking a non-terminal up in the grammar environment.
--
-- 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.
--
-- == What used to be here
--
-- 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
  ( Lookup
  , Names
  ) where

import GHC.TypeLits (ErrorMessage (..), Symbol, TypeError)

import PEG.Type

-- | Look up a non-terminal's entry in the environment.
--
-- 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.  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@.)
type family Lookup (s :: Symbol) (env :: Env) :: EnvEntry where
  Lookup s env = Found s env (LookupMb s env)

type family LookupMb (s :: Symbol) (env :: Env) :: Maybe EnvEntry where
  LookupMb _ '[]               = 'Nothing
  LookupMb s ('(s, e) ': rest) = 'Just e
  LookupMb s (_ ': rest)       = LookupMb s rest

type family Found (s :: Symbol) (env :: Env)
                  (r :: Maybe EnvEntry) :: EnvEntry where
  Found _ _ ('Just e) = e
  Found s env 'Nothing =
    TypeError ('Text "Undefined non-terminal: " ':<>: 'ShowType s
         ':$$: 'Text "Available non-terminals: " ':<>: 'ShowType (Names env))

-- | The names an environment defines, for the message above.
type family Names (env :: Env) :: [Symbol] where
  Names '[]               = '[]
  Names ('(s, _) ': rest) = s ': Names rest