packages feed

typed-peg-0.1.0.0: src/PEG/Parse.hs

{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE GADTs               #-}
{-# LANGUAGE RankNTypes          #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications    #-}
{-# LANGUAGE TypeFamilies        #-}
{-# LANGUAGE TypeOperators       #-}

-- | Running a 'Grammar' against a 'String'.
--
-- The top-level entry points are 'parse' (uses 'defaultOpts') and 'parseWith'
-- (accepts custom 'Opts' for indentation-sensitive parsing).  Both return a
-- 'Result' that records the matched value, the consumed prefix, and the
-- remaining suffix.
module PEG.Parse
  ( Result (..)
  , parse
  , parseWith
  , eval
  , Opts (..)
  , defaultOpts
  , Input
  , PState (..)
  , columns
  ) where

import PEG.Grammar
import PEG.Indent
import PEG.Member
import PEG.Syntax
import PEG.Type
import PEG.TyLevel (Lookup)

-- | The result of running a grammar.
--
-- @'OK' a consumed rest@ means the grammar matched, producing value @a@.
-- @consumed@ is the prefix of the input that was consumed; @rest@ is the
-- remaining input.
data Result a
  = OK a String String
  | Fail
  deriving (Show, Eq)

-- | A string annotated with column positions, as produced by 'columns'.
type Input = [(Char, Int)]

-- | Internal parser state.
data PState = PState
  { stInput :: Input     -- ^ Remaining input with column positions.
  , stCands :: !Interval -- ^ Current candidate column interval.
  , stAlign :: !Bool     -- ^ Whether the next token must be aligned.
  }

-- | Annotate every character in a string with its column position.
-- Tab stops are expanded according to @tabWidth@.
columns :: Int -> String -> Input
columns tabWidth = go 0
  where
    go _ []     = []
    go c (x:xs) = (x, c) : go (next c x) xs

    next _ '\n' = 0
    next c '\t'
      | tabWidth > 1 = ((c `div` tabWidth) + 1) * tabWidth
      | otherwise    = c + 1
    next c _    = c + 1

-- | Parser configuration.
data Opts = Opts
  { optTokenMode :: RelD     -- ^ Default column relation between tokens.
  , optCands     :: Interval -- ^ Initial candidate column interval.
  , optTabWidth  :: Int      -- ^ Number of columns per tab stop.
  }

-- | Default options: accept tokens at any column, tab width of 8.
defaultOpts :: Opts
defaultOpts = Opts
  { optTokenMode = relD anyR
  , optCands     = fullI
  , optTabWidth  = 8
  }

-- | Run a grammar with 'defaultOpts'.
parse :: Grammar env ty a -> String -> Result a
parse = parseWith defaultOpts

-- | Run a grammar with custom 'Opts'.
parseWith :: Opts -> Grammar env ty a -> String -> Result a
parseWith opts (Grammar rules start) input =
  case eval rules start (optTokenMode opts) st0 of
    Nothing      -> Fail
    Just (a, st) ->
      let n = length input - length (stInput st)
      in OK a (take n input) (drop n input)
  where
    st0 = PState
      { stInput = columns (optTabWidth opts) input
      , stCands = optCands opts
      , stAlign = False
      }

-- | Low-level evaluator: run a 'PExp' against a 'PState' under a given column
-- relation.  Exposed for advanced use; most callers should use 'parse' or
-- 'parseWith'.
eval :: forall env ty a
      . Rules env env
     -> PExp env ty a
     -> RelD
     -> PState
     -> Maybe (a, PState)
eval rules = go
  where
    go :: forall t b. PExp env t b -> RelD -> PState -> Maybe (b, PState)
    go (Pure x) _ st = Just (x, st)

    go (Term c) tau st = do
      (x, st') <- terminal tau st
      if x == c then Just (c, st') else Nothing

    go AnyChar tau st = terminal tau st

    go (NT (_ :: Name s)) tau st =
      go (ruleFor (member :: Member s env (TyOf (Lookup s env))
                                          (ResOf (Lookup s env)))
                  rules)
         tau st

    go (Seq ef ex) tau st = do
      (f, st')  <- go ef tau st
      (x, st'') <- go ex tau st'
      pure (f x, st'')

    go (Choice e1 e2) tau st = case go e1 tau st of
      Just r  -> Just r
      Nothing -> go e2 tau st

    go (Star e) tau st = Just (starLoop (go e tau) st)

    go (Not e) tau st = case go e tau st of
      Just _  -> Nothing
      Nothing -> Just ((), st)

    go (Map f e) tau st = do
      (x, st') <- go e tau st
      pure (f x, st')

    go (Indent rho e) tau st = do
      (x, st') <- go e tau st { stCands = preimage rd (stCands st) }
      pure ( x
           , st' { stCands = interI (stCands st) (image rd (stCands st')) } )
      where
        rd = relD rho

    go (Position sigma e) _ st = go e (relD sigma) st

    go (Align e) tau st = do
      (x, st') <- go e tau st { stAlign = True }
      pure (x, st' { stAlign = stAlign st && stAlign st' })

terminal :: RelD -> PState -> Maybe (Char, PState)
terminal tau (PState input cands aligned) = case input of
  []            -> Nothing
  ((x, i) : xs)
    | aligned   ->
        if memberI i cands
          then Just (x, PState xs (singletonI i) False)
          else Nothing
    | otherwise ->
        if memberI i (preimage tau cands)
          then Just (x, PState xs (interI cands (image tau (singletonI i))) False)
          else Nothing

starLoop :: (PState -> Maybe (a, PState)) -> PState -> ([a], PState)
starLoop step = loop
  where
    loop st = case step st of
      Nothing       -> ([], st)
      Just (x, st') -> let (xs, rest) = loop st' in (x : xs, rest)

ruleFor :: Member s defs ty a -> Rules env defs -> PExp env ty a
ruleFor Here      (RCons _ body _)    = body
ruleFor (There m) (RCons _ _    rest) = ruleFor m rest