将BNF翻译成Parsec程序有什么诀窍吗?

时间:2015-03-03 09:47:49

标签: parsing haskell compiler-construction grammar parsec

匹配函数调用链的BNF(如x(y)(z)...):

expr = term T
T    = (expr) T
      | EMPTY
term = (expr)
      | VAR 

将其转换为看起来很棘手的Parsec程序。

term :: Parser Term
term = parens expr <|> var

expr :: Parser Term
expr = do whiteSpace
          e <- term
          maybeAddSuffix e
  where addSuffix e0 = do e1 <- parens expr
                          maybeAddSuffix $ TermApp e0 e1
        maybeAddSuffix e = addSuffix e
                           <|> return e

您能否列出有关将BNF翻译成Parsec程序的所有设计模式?

1 个答案:

答案 0 :(得分:3)

如果您的语法相当大,最简单的想法就是使用Alex / Happy组合。它使用相当简单,直接接受BNF格式 - 不需要人工翻译 - 也许最重要的是,产生极快的解析器/词法分析器。

如果您已经决定使用parsec(或者您正在将此作为学习练习),我发现通常可以分两个阶段进行操作。先lexing,然后解析。 Parsec会做两件事!

首先编写适当的类型:

{-# LANGUAGE LambdaCase #-}

import Text.Parsec 
import Text.Parsec.Combinator 
import Text.Parsec.Prim
import Text.Parsec.Pos
import Text.ParserCombinators.Parsec.Char 
import Control.Applicative hiding ((<|>))
import Control.Monad 

data Term = App Term Term | Var String deriving (Show, Eq)

data Token = LParen | RParen | Str String deriving (Show, Eq)

type Lexer = Parsec [Char] ()   -- A lexer accepts a stream of Char
type Parser = Parsec [Token] () -- A parser accepts a stream of Token

解析单个令牌很简单。为简单起见,变量是1个或更多个字母。你当然可以随意更改它。

oneToken :: Lexer Token
oneToken = (char '(' >> return LParen) <|> 
           (char ')' >> return RParen) <|>
           (Str <$> many1 letter)

解析整个令牌流只是多次解析单个令牌,可能由空格分隔:

lexer :: Lexer [Token]
lexer = spaces >> many1 (oneToken <* spaces) 

注意spaces的位置:这样,在字符串的开头和结尾都接受空格。

由于Parser使用自定义令牌类型,因此您必须使用自定义satisfy功能。幸运的是,这几乎与现有的满足相同。

satisfy' :: (Token -> Bool) -> Parser Token
satisfy' f = tokenPrim show 
                       (\src _ _ -> incSourceColumn src 1) 
                       (\x -> if f x then Just x else Nothing)

然后我们可以为每个原始令牌编写解析器。

lparen = satisfy' $ \case { LParen -> True ; _ -> False } 
rparen = satisfy' $ \case { RParen -> True ; _ -> False } 
strTok = (\(Str s) -> s) <$> (satisfy' $ \case { Str {} -> True ; _ -> False })

如您所想,parens对我们的目的有用。写作非常简单。

parens :: Parser a -> Parser a 
parens = between lparen rparen 

现在有趣的部分。

term, expr, var :: Parser Term

term = parens expr <|> var

var = Var <$> strTok 

这两个对你来说应该是相当明显的。

Parec包含组合器optionoptionMaybe,当您需要“可能做某事”时,这些组合非常有用。

expr = do 
  e0 <- term 
  option e0 (parens expr >>= \e1 -> return (App e0 e1))

最后一行表示 - 尝试应用提供给option的解析器 - 如果失败,则返回e0

测试时你可以这样做:

tokAndParse = runParser (lexer <* eof) () "" >=> runParser (expr <* eof) () ""

每个解析器附加的eof是为了确保消耗整个输入;如果有额外的尾随字符,则字符串不能是语法的成员。注意 - 您的示例x(y)(z)实际上并不在您的语法中!

>tokAndParse "x(y)(z)"
Left (line 1, column 5):
unexpected LParen
expecting end of input

但以下是

>tokAndParse "(x(y))(z)"
Right (App (App (Var "x") (Var "y")) (Var "z"))