You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何我的Parsec词法分析器仅对整数存在空格敏感性?

问题:Parsec解析器无法处理无空格分隔的=>与整数

我编写了如下解析器代码:

module Parser2 where

import Text.Parsec
import Text.Parsec.String (Parser)
import Text.Parsec.Language (emptyDef)

import qualified Text.Parsec.Expr as Ex
import qualified Text.Parsec.Token as Tok

data Expr
  = Map [(Expr, Expr)]
  | Int Integer
  | Str String
  deriving (Eq, Ord, Show)

lexer :: Tok.TokenParser ()
lexer = Tok.makeTokenParser style
  where
    ops = ["=>"]
    names = []
    style = emptyDef {
               Tok.commentLine = "#"
             , Tok.reservedOpNames = ops
             , Tok.reservedNames = names
             }

integer :: Parser Integer
integer = Tok.integer lexer

commaSep :: Parser a -> Parser [a]
commaSep = Tok.commaSep lexer

reserved :: String -> Parser ()
reserved = Tok.reserved lexer

int :: Parser Expr
int = Int <$> integer

maplit :: Parser Expr
maplit = do
    reserved "{"
    c <- commaSep $ do
        k <- expr
        reserved "=>"
        v <- expr
        return (k, v)
    reserved "}"
    return $ Map c

expr :: Parser Expr
expr = Ex.buildExpressionParser [] factor

stringLit :: Parser Expr
stringLit = Str <$> Tok.stringLiteral lexer

factor :: Parser Expr
factor = try stringLit
      <|> try maplit
      <|> try int

contents :: Parser a -> Parser a
contents p = do
  Tok.whiteSpace lexer
  r <- p
  eof
  return r

parseExpr :: String -> Either ParseError Expr
parseExpr = parse (contents expr) "<stdin>"

该解析器可正确解析整数前后带空格的映射,但无法解析整数前无空格的情况,而字符串不受此影响:

ghci> parseExpr "{\"a\"=> 1,\"b\"=> 2}"
Right (Map [(Str "a",Int 1),(Str "b",Int 2)])

ghci> parseExpr "{\"b\"=>\"c\"}"
Right (Map [(Str "b",Str "c")])

ghci> parseExpr "{\"a\"=>1}"
Left "<stdin>" (line 1, column 8):
unexpected '1'
expecting end of "=>"

我意识到这可能与makeTokenParser默认设置有关,但不清楚具体原因,也未找到合适调试方法,恳请帮助。


原因分析

核心问题是错误使用了reserved函数匹配操作符=>:

  • Tok.reserved是用于匹配保留字的,它要求匹配的字符串后面必须跟着一个词法边界(空白、括号、引号等非标识符字符)。
  • 你把"=>"放到了reservedOpNames列表中,但用reserved而非reservedOp来匹配它,导致解析=>1时,reserved "=>"匹配完符号后,发现后续的1属于标识符(整数词法单元),不满足词法边界要求,直接报错。
  • 字符串不受影响是因为引号本身就是词法边界,"=>"后接引号时,reserved会认为符合边界要求,因此能继续解析。

解决方案

把匹配"=>"的代码从reserved "=>"改成Tok.reservedOp lexer "=>"——reservedOp是专门用于匹配操作符的,没有词法边界的限制:

maplit :: Parser Expr
maplit = do
    reserved "{"
    c <- commaSep $ do
        k <- expr
        Tok.reservedOp lexer "=>"  -- 替换为reservedOp
        v <- expr
        return (k, v)
    reserved "}"
    return $ Map c

另外补充:用reserved匹配{和}也不合适,它们属于标点符号,更推荐用Tok.symbol lexer "{"或Tok.braces处理,但这不是当前问题的核心。修改后,parseExpr "{\"a\"=>1}"即可正常解析。

调试建议

  • 使用Parsec的traceParser函数跟踪解析流程,比如修改expr为:
    expr = traceParser "expr" (Ex.buildExpressionParser [] factor)
    
    能直观看到每一步的解析尝试。
  • 明确Text.Parsec.Token中核心函数的差异:
    • reserved: 匹配保留字,要求前后有词法边界
    • reservedOp: 匹配操作符,无边界要求
    • symbol: 匹配任意符号,自动处理前后空白

内容的提问来源于stack exchange,提问作者Tanse

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 07:16:14