【问题标题】:Parsec.Expr repeated Prefix with different priorityParsec.Expr 以不同优先级重复 Prefix
【发布时间】:2015-10-19 12:16:07
【问题描述】:

Parsec.Expr.buildExpressionParser 的文档说:

具有相同优先级的前缀和后缀运算符只能出现一次(即,如果 - 是前缀否定,则不允许出现 --2)。

但是,我想解析这样的字符串。

具体来说,考虑以下语法:

sentence: 
    | identifier
    | "~" sentence
    | sentence & sentence
    | "!" sentence

其中运算符优先级为:"~" 绑定强于 "&" 绑定强于 "!"

例如,我想要句子

! ~a & b

被解析为

! ( (~a) & b )

还有句子

~ ! a & b 

作为

~( ! ( a & b) )

Parsec 允许我这样做(并指定运算符优先级),但是,我希望能够链接前缀,例如~ ~ ! ~ a。 Parsec 不允许这样做。 我找到了解决方案for chaining prefixes,但是这个解决方案不允许我为不同的前缀运算符指定不同的运算符优先级(“~”和“!”绑定比“&”强,或者它们都没有)

有人对此有解决方案吗?

编辑:

使操作符绑定正确但不允许链接的部分解决方案: http://lpaste.net/143362

使用链接但对“~”运算符有错误绑定的部分解决方案: http://lpaste.net/143364

编辑:latest answer 相关的更多说明。

我实际上希望& 具有关联性。左或右无所谓。左与右关联仅在具有相同优先级的运算符之间起作用。 对于您的示例,通过注意 & 绑定比 ! 更强(& 具有更高的运算符优先级)来解决所有问题

因此,你担心的表情:

a & ! b & c 应该变成: (尽可能先绑定&a & ! (b & c)

同样,! a & ! b & c 应该被解析 (先绑定&) ! a & ! (b & c),因此! a & (! (b & c)),因此! (a & (! (b & c)))

【问题讨论】:

  • 你能展示你的部分解决方案吗?我在lpaste.net/143362 编码了一些东西,它不做链接或重复前缀 - 它只是试图让优先级正确。
  • 我有两个部分解决方案。其中一个看起来很像您的编码,并忽略了重复的前缀。另一个进行链接,但优先级错误。 (他们正在路上)
  • 我添加了部分解决方案。准确地说,我养了你的一个,然后从那个开始,得到另一个……

标签: haskell parsec


【解决方案1】:

我对我原来的答案不满意,因为它不能解决各种优先级的前缀和后缀运算符的一般情况,它要求程序员必须考虑语法,而不是仅仅依赖 buildExpressionParser做正确的事。

我在网上四处搜寻,发现了Pratt method for recursive descent parsing of expressions。我能够实现一个紧凑的 Haskell 版本来替换 buildExpressionParser。它具有与buildExpressionParser 完全相同的接口,但不需要您使用链式前缀组合器或使用术语解析器。我玩弄了您的语法,更改了& 的关联性,并将前缀运算符切换为后缀运算符,这一切似乎都有效......

buildPrattParser table termP = parser precs where

  precs = reverse table

  prefixP = choice prefixPs <|> termP where
    prefixPs = do
      precsR@(ops:_) <- tails precs 
      Prefix opP <- ops
      return $ opP <*> parser precsR

  infixP precs lhs = choice infixPs <|> pure lhs where
    infixPs = do
      precsR@(ops:precsL) <- tails precs
      op <- ops
      p <- case op of
        Infix opP assoc -> do
          let p precs = opP <*> pure lhs <*> parser precs
          return $ case assoc of
            AssocNone  -> error "Non associative operators are not supported"
            AssocLeft  -> p precsL
            AssocRight -> p precsR
        Postfix opP ->
          return $ opP <*> pure lhs
        Prefix _ -> mzero
      return $ p >>= infixP precs

  parser precs = prefixP >>= infixP precs

【讨论】:

  • 请注意,这(还)不检测以相同优先级使用左右关联运算符,或处理非关联运算符。如果您的语法不包含此类运算符,那么您应该很高兴!
【解决方案2】:

我在http://lpaste.net/143362 的部分解决方案的一个问题是它无法识别~ ! a

但是,如果您将运算符表更改为:

table   = [ [ Prefix tilde ]
          , [ Infix amper AssocLeft ]
          , [ Prefix bang ]
          , [ Prefix tilde ]
          ]

它可以正确解析该表达式以及! ~a &amp; b~ ! a &amp; b。代码在:http://lpaste.net/143370

所以现在把这个想法和你的链接结合起来试试:

table   = [ [ Prefix (chained tilde) ]
          , [ Infix amper AssocLeft ]
          , [ Prefix (chained bang) ]
          , [ Prefix (chained tilde) ]
          ]

chained  p = chainl1 p $ return (.)

代码在:http://lpaste.net/143371

【讨论】:

  • 非常感谢您的努力。但是,这个解决方案仍然存在两个问题:1.它不解析! ~ ! a形式的表达式2.它解析~ a &amp; b错误(将其解析为~ (a &amp; b)而不是(~a) &amp; b
【解决方案3】:

你想要的解析器的左因子语法是:

sentence : '!' sentence
         | sentence1

sentence1 : sentence2 '&' sentence1
          | sentence2

sentence2 : '~' sentence2
          | term

term : '!' sentence
     | ident

在 EBNF 中可以改写为:

sentence : '!'* sentence1

sentence1 : sentence2 ('&' sentence2)*

sentence2 : '~'* term

term : '!' sentence
     | ident

buildExpressionParser 使用链式前缀运算符生成的解析器几乎生成了这个解析器,只是它在术语解析器中不包含! 规则;因此在~ 之后遇到! 时会出现解析错误。

鉴于以下情况:

{-# LANGUAGE NoMonomorphismRestriction #-}
module Main where

import Control.Monad
import Text.Parsec
import Text.Parsec.Expr
import Text.Parsec.Char
import Control.Applicative ( (<*), (*>), (<*>), (<$), (<$>) )

data Sentence = Tilde Sentence
              | Bang Sentence
              | Amper Sentence Sentence
              | Ident String
  deriving ( Eq, Ord, Show )

bangP  = Bang  <$ lexeme (char '!')
amperP = Amper <$ lexeme (char '&')
tildeP = Tilde <$ lexeme (char '~')
identP = Ident <$> lexeme (many1 alphaNum)

lexeme = (<* spaces)

parser = spaces *> sentence <* eof

main = do
  let inputs = [ "a", "! a", "~ a", "a & b", "! a & b"
               , "~ a & b", "! ~ a & b", "~ ! a & b", "! ~ ! a"
               , "~ a & b", "a & ! b & c & d"
               ]
  forM_ inputs $ \input -> do
    putStr input
    putStr " -> "
    parseTest parser input

我们可以手动定义sentence解析器:

sentence = sentence0 where
  sentence0 = chainl bangP (return (.)) id <*> sentence1
  sentence1 = chainl1 sentence2 amperP
  sentence2 = chainl tildeP (return (.)) id <*> term
  term = (bangP <*> sentence0) <|> identP

如果我们将! 规则添加到term 解析器中,我们可以使用buildExpressionParser

sentence = buildExpressionParser table term where
  table = [ [prefix tildeP]
          , [Infix amperP AssocLeft]
          , [prefix bangP]
          ]
  term = (bangP <*> sentence) <|> identP
  prefix  p = Prefix . chainl1 p $ return (.)

【讨论】:

  • 这看起来真的很像我想要的。尤其是 buildExpressionParser 方法看起来不错。
【解决方案4】:

一个新的答案...

你有没有想过 & 运算符的结合性?

这是我提出的另一个想法,假设 & 是右结合的。

  1. 收集词条之前的前缀运算符序列。
  2. 解析术语(标识或括号表达式)
  3. 通过从步骤 1 中收集的序列中转移 ~ 运算符来修正术语。
  4. 如果下一个标记是 &,则安培运算符的 LHS 是固定项。其余的运算符应用于 amper 表达式。
  5. 否则结果只是应用于术语的前缀运算符。

我相信 & 的关联性很重要,例如我们有:

a & ! b & c  -->   a & (! b & c)  --> a & ! (b & c)

a & ! b & c  -->   (a & (! b)) & c

另一个需要考虑的情况是! a &amp; ! b &amp; c - 你想如何解析它?

一个实现:

 {-# LANGUAGE NoMonomorphismRestriction, FlexibleContexts #-}

 import Text.Parsec
 import Control.Monad
 import Text.ParserCombinators.Parsec hiding (runParser, try)
 import Text.Parsec.Char

 data Sentence = Ident String | Bang Sentence | Tilde Sentence | Amper Sentence Sentence
   deriving (Show)

 lexer p = do x <- p; spaces; return x
 ident = lexer (many1 letter)
 sym ch  = lexer (char ch)

 tilde = sym '~'
 bang  = sym '!'
 amper = sym '&'

 parens p = between (sym '(') (sym ')') p

 term    =  parens expr 
          <|> (fmap Ident ident)
          <?> "simple expression"

 prefixOps = many (try tilde <|> bang)

 expr = do
   ops <- fmap reverse prefixOps
   lhs <- term

   let (ops', lhs') = popTildes ops lhs
       pre = mkPrefixNode ops'

   mrhs <- try (fmap Just (amper >> expr)) <|> (return Nothing)

   case mrhs of
     Nothing  -> return $ pre lhs'
     Just rhs -> return $ pre (Amper lhs' rhs)

 popTildes :: [Char] -> Sentence -> ([Char], Sentence)
 popTildes ('~':rest) s = popTildes rest (Tilde s)
 popTildes ops s        = (ops, s)

 mkPrefixNode :: [Char] -> (Sentence -> Sentence)
 mkPrefixNode [] = id
 mkPrefixNode ('~':rest) = mkPrefixNode rest . Tilde
 mkPrefixNode ('!':rest) = mkPrefixNode rest . Bang 
 mkPrefixNode _          = error "can't happen"

 check :: String -> IO ()
 check input = do
   let padded = input ++ (replicate (15-length input) ' ')
   case parse expr "-" input of
     Left e  -> do putStrLn $ "FAILED: " ++ input
                   putStrLn $ "  " ++ show e
     Right x -> do putStrLn $ "OK: " ++ padded ++ " -> " ++ show x

 inputs = [ "a", "! a", "~ a", "a & b", "! a & b", "~ a & b", "! ~ a & b"
          ,  "~ ! a", "! ~a & b", "~ ! a & b ", "! ~ ! a 2"
          ]

 main = mapM_ check inputs

【讨论】:

  • 非常感谢。这是一个非常有用的答案。该解决方案可以按我的意愿解析事物。我注意到您现在基本上是手动进行所有解析(不再使用 BuildExpressionParser)。我仍然需要看看我是否可以将它集成到我的实际示例中(这要复杂得多,有更多的二元和一元运算符 -> 这就是我试图避免自己构建表达式解析器的原因)跨度>
  • 我的感觉是你不能使用 buildExpressionParser 来做到这一点。它生成的解析器是非常分层的,它不执行任何前瞻。对于每个优先级,它生成一个解析器,其中术语是前一级别的解析器识别的内容,运算符是当前优先级的那些。阅读代码可能会有所启发 - 这实际上是一个非常简单的想法:(link)。任何,祝你好运,我很想知道你想出了什么。
猜你喜欢
  • 2021-03-06
  • 2019-12-06
  • 1970-01-01
  • 1970-01-01
  • 2011-06-29
  • 2013-03-16
  • 1970-01-01
  • 1970-01-01
  • 2017-11-16
相关资源
最近更新 更多