8

我正在尝试使用 Parsec 编写一个解析器,它将解析读写的 Haskell 文件,例如:

The classic 'Hello, world' program.

\begin{code}

main = putStrLn "Hello, world"

\end{code}

More text.

我写了以下内容,有点受 RWH 中示例的启发:

import Text.ParserCombinators.Parsec

main
    = do contents <- readFile "hello.lhs"
         let results = parseLiterate contents
         print results

data Element
    = Text String
    | Haskell String
    deriving (Show)


parseLiterate :: String -> Either ParseError [Element]

parseLiterate input
    = parse literateFile "(unknown)" input



literateFile
    = many codeOrProse

codeOrProse
    = code <|> prose

code
    = do eol
         string "\\begin{code}"
         eol
         content <- many anyChar
         eol
         string "\\end{code}"
         eol
         return $ Haskell content

prose
    = do content <- many anyChar
         return $ Text content

eol
    =   try (string "\n\r")
    <|> try (string "\r\n")
    <|> string "\n"
    <|> string "\r"
    <?> "end of line"

我希望这会产生以下结果:

[Text "The classic 'Hello, world' program.", Haskell "main = putStrLn \"Hello, world\"", Text "More text."]

(允许空格等)。

这编译得很好,但是在运行时,我得到了错误:

*** Exception: Text.ParserCombinators.Parsec.Prim.many: combinator 'many' is applied to a parser that accepts an empty string

任何人都可以对此有所了解,并可能提供解决方案吗?

4

3 回答 3

9

正如所指出的那样many anyChar是问题所在。但不仅在,prose而且在code. 问题code是,这content <- many anyChar将消耗一切:换行符和\end{code}标签。

所以,你需要有一些方法来区分散文和代码。一个简单(但可能太天真)的方法是寻找反斜杠:

literateFile = many codeOrProse <* eof

code = do string "\\begin{code}"
          content <- many $ noneOf "\\"
          string "\\end{code}"
          return $ Haskell content

prose = do content <- many1 $ noneOf "\\"
           return $ Text content

现在,您并没有完全得到想要的结果,因为该Haskell部分还将包含换行符,但您可以很容易地将它们过滤掉(给定一个filterNewlines您可以说的函数`content <- filterNewlines <$> (many $ noneOf "\\"))。

编辑

好的,我想我找到了一个解决方案(需要最新的 Parsec 版本,因为lookAhead):

import Text.ParserCombinators.Parsec
import Control.Applicative hiding (many, (<|>))

main
    = do contents <- readFile "hello.lhs"
         let results = parseLiterate contents
         print results

data Element
    = Text String
    | Haskell String
    deriving (Show)    

parseLiterate :: String -> Either ParseError [Element]

parseLiterate input
    = parse literateFile "" input

literateFile
    = many codeOrProse

codeOrProse = code <|> prose

code = do string "\\begin{code}\n"
          c <- untilP (string "\\end{code}\n")
          string "\\end{code}\n"
          return $ Haskell c

prose = do t <- untilP $ (string "\\begin{code}\n") <|> (eof >> return "")
           return $ Text t

untilP p = do s <- many $ noneOf "\n"
              newline
              s' <- try (lookAhead p >> return "") <|> untilP p
              return $ s ++ s'

untilP p解析一行,然后检查下一行的开头是否可以被成功解析p。如果是,则返回空字符串,否则继续。是必需的lookAhead,因为否则开始\结束标签将被消耗并且code无法识别它们。

我想它仍然可以更简洁(即不必在string "\\end{code}\n"里面重复code)。

于 2011-10-13T12:44:28.327 回答
7

我没有测试过,但是:

  • many anyChar可以匹配一个空字符串
  • 因此prose可以匹配一个空字符串
  • 因此codeOrProse可以匹配一个空字符串
  • 因此literateFile可以永远循环,匹配无限多的空字符串

更改prose为匹配many1字符可能会解决此问题。

(我对 Parsec 不是很熟悉,但是怎么prose知道它应该匹配多少个字符?它可能会消耗整个输入,永远不会给code解析器第二次机会来寻找新代码段的开头。或者它可能只在每次调用中匹配一个字符,从而使many/many1中的 / 无用。)

于 2011-10-13T12:29:24.137 回答
1

作为参考,这是我想出的另一个版本(稍微扩展以处理其他情况):

import Text.ParserCombinators.Parsec

main
    = do contents <- readFile "test.tex"
         let results = parseLiterate contents
         print results

data Element
    = Text String
    | Haskell String
    | Section String
    deriving (Show)

parseLiterate :: String -> Either ParseError [Element]

parseLiterate input
    = parse literateFile "(unknown)" input

literateFile
    = do es <- many elements
         eof
         return es

elements
    = try section
  <|> try quotedBackslash
  <|> try code
  <|> prose

code
    = do string "\\begin{code}"
         c <- anyChar `manyTill` try (string "\\end{code}")
         return $ Haskell c

quotedBackslash
    = do string "\\\\"
         return $ Text "\\\\"

prose
    = do t <- many1 (noneOf "\\")
         return $ Text t

section
    = do string "\\section{"
         content <- many1 (noneOf "}")
         char '}'
         return $ Section content
于 2011-10-14T20:20:16.863 回答