如何使嵌套的 megaparsec 解析器失败?

How to fail a nested megaparsec parser?

我遇到了以下解析问题:

解析一些可能包含来自有限字符集的零个或多个元素的文本字符串,最多但不包括一组终止字符中的一个。 Content/no内容应通过Maybe表示。终止字符可能以转义形式出现在字符串中。任何不允许的字符解析都应该失败。

这是我想出的(简化):

import qualified Text.Megaparsec as MP

-- Predicate for admissible characters, not including the control characters.
isAdmissibleChar :: Char -> Bool
...

-- Predicate for control characters that need to be escaped.
isControlChar :: Char -> Bool
...

-- The escape character.
escChar :: Char
...


pComponent :: Parser (Maybe Text)
pComponent = do
  t <- MP.many (escaped <|> regular)
  if null t then return Nothing else return $ Just (T.pack t)
 where
  regular = MP.satisfy isAdmissibleChar <|> fail "Inadmissible character"
  escaped = do
    _ <- MC.char escChar
    MP.satisfy isControlChar -- only control characters may be escaped

比如说,允许的字符是大写的ASCII,转义是'\',控制是':'。 然后,以下正确解析:ABC\:D:EF 产生 ABC:D。 但是,解析 ABC&D,其中 & 是不可接受的,确实会产生 ABC 而我希望得到一条错误消息。

两个问题:

fail一般不会结束解析。它只是继续下一个选择。在这种情况下,它选择了由 many 组合器引入的空列表替代项,因此它会停止解析而不会出现错误消息。

我认为解决你的问题最好的方法是指定输入必须以终止符结尾,也就是说不能这样“中途”“成功”。您可以使用 notFollowedBy or lookAhead combinators. Here is the relevant part of the megaparsec tutorial.

many 必须允许其子解析器在没有整个解析的情况下失败一次 失败 - 例如 many (char 'A') *> char 'B',解析时 “AAAB”,必须无法解析 B 才能知道它到达了结尾 作为.

您可能需要 manyTill 来识别终结符 明确地。像这样:

MP.manyTill (escaped <|> regular) (MP.satisfy isControlChar)

如果 isControlChar 不接受“&”,“ABC&D”会在此处给出错误。

或者,如果您想解析多个组件,您可以保留 pComponent 的现有定义并将其与 sepBy 或类似的一起使用,例如:

MP.sepBy pComponent (MP.satisfy isControlChar)

如果您在此之后还检查文件结尾,例如:

MP.sepBy pComponent (MP.satisfy isControlChar) <* MP.eof

那么 "ABC&D" 应该会再次报错,因为 '&' 将结束第一个组件,但不会被接受为分隔符。

解析器对象通常所做的是从输入流中提取它应该接受的任何子集。这是通常的规则。

在这里,您似乎希望解析器接受后跟[=​​42=] 特定内容的字符串。根据您的示例,它是文件结尾 (eof) 或字符“:”。所以你可能要考虑 向前看.

环境及辅助功能:


import            Data.Void  (Void)
import qualified  Data.Text        as  T
import qualified  Text.Megaparsec  as  MP
import qualified  Text.Megaparsec.Char  as  MC

type Parser = MP.Parsec Void T.Text

-- Predicate for admissible characters, not including the control characters.
isAdmissibleChar :: Char -> Bool
isAdmissibleChar ch  =  elem ch ['A' .. 'Z']

-- Predicate for control characters that need to be escaped.
isControlChar :: Char -> Bool
isControlChar ch = elem ch ":"

-- The escape character:
escChar :: Char
escChar = '\'

终止解析器,用于前瞻:

termination :: Parser ()
termination = MP.eof  MP.<|>  do
                                  _ <- MP.satisfy isControlChar
                                  return ()

修改后的 pComponent 解析器:

pComponent :: Parser (Maybe T.Text)
pComponent = do
    txt <- MP.many (escaped  MP.<|>  regular)
    MP.lookAhead  termination  --  **CHANGE HERE** 
    if (null txt)  then  (return Nothing)  else  (return $ Just (T.pack txt))
 where
   regular = (MP.satisfy isAdmissibleChar)  MP.<|>  (fail "Inadmissible character")
   escaped = do
     _ <- MC.char escChar
     MP.satisfy isControlChar -- only control characters may be escaped

测试实用程序:

tryParse :: String -> IO ()
tryParse str = do
    let  res = MP.parse  pComponent  "(noname)"  (T.pack str)
    putStrLn $ (show res)

让我们尝试重新运行您的示例:

$ ghci
 λ> 
 λ> :load q67809465.hs
 λ>
 λ> str1 = "ABC\:D:EF"
 λ> putStrLn str1
 ABC\:D:EF
 λ> 
 λ> tryParse str1
 Right (Just "ABC:D")
 λ> 

这样就成功了,如愿。

 λ> 
 λ> tryParse "ABC&D"
Left (ParseErrorBundle {bundleErrors = TrivialError 3 (Just (Tokens ('&' :| ""))) (fromList [EndOfInput]) :| [], bundlePosState = PosState {pstateInput = "ABC&D", pstateOffset = 0, pstateSourcePos = SourcePos {sourceName = "(noname)", sourceLine = Pos 1, sourceColumn = Pos 1}, pstateTabWidth = Pos 8, pstateLinePrefix = ""}})
 λ> 

所以失败了,如愿。

尝试我们的 2 个可接受的终止上下文:

 λ> tryParse "ABC:&D"
 Right (Just "ABC")
 λ> 
 λ> 
 λ> tryParse "ABCDEF"
 Right (Just "ABCDEF")
 λ>