Parsec:使用以“$”(无空格)开头的变量解析表达式

Parsec: parsing expressions with variables that start with '$' (no whitespace)

我想使用 Parsec 的 Token 和 Expr 模块解析包含以 $ 开头的变量的表达式(如 $a=$b)。这是我的代码的简化版本:

module Main where

import Control.Monad.Identity
import Control.Applicative

import Text.Parsec
import Text.Parsec.String

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

data Expr
  = BinaryOp String Expr Expr
  | Var String
  deriving (Show)

lexer :: Tok.TokenParser ()
lexer = Tok.makeTokenParser style
  where
    style = Tok.emptyDef
      { Tok.reservedOpNames = ["="]
      , Tok.reservedNames   = []
      , Tok.identStart      = letter
      , Tok.identLetter     = alphaNum
      }

reservedOp = Tok.reservedOp lexer
identifier = Tok.identifier lexer
whiteSpace = Tok.whiteSpace lexer

parseExpr :: String -> Either ParseError Expr
parseExpr = parse (whiteSpace *> expr <* eof) ""

expr :: Parser Expr
expr = Ex.buildExpressionParser opTable terms <?> "expression"
  where
    opTable =
      [ [ Ex.Infix (reservedOp "=" >> return (BinaryOp "=")) Ex.AssocLeft ] ]
    terms =
      try var

var :: Parser Expr
var = Var <$> (char '$' >> identifier)

--

main :: IO ()
main = case parseExpr "$a=$b" of
  Left err -> print err
  Right expr -> print expr

这适用于运算符周围有空格的表达式(如 $a = $b),但没有空格($a=$b)我得到错误:

(line 1, column 5):
unexpected '$'
expecting operator

此外,如果我修改解析器以解析不以 $ 开头的变量,则解析器可以使用和不使用空格。所以 $ 和它们之间没有空格的运算符的组合似乎有问题。

问题出在默认标记解析器中 opStartopLetter 的定义:

emptyDef   :: LanguageDef st
emptyDef    = LanguageDef
               { commentStart   = ""
               ...
               , opStart        = opLetter emptyDef
               , opLetter       = oneOf ":!#$%&*+./<=>?@\^|-~"
               ...
               }

令牌解析器使用 opStartopLetter 贪婪地匹配运算符名称,因此 $a=$b 解析与 $a =$ b 相同。由于 =$ 不是运算符,因此您会收到语法错误。

如果您从 opLetter 中删除 $,事情应该会起作用,例如:

lexer :: Tok.TokenParser ()
lexer = Tok.makeTokenParser style
  where
    style = Tok.emptyDef
      { Tok.reservedOpNames = ["="]
      , Tok.reservedNames   = []
      , Tok.identStart      = letter
      , Tok.identLetter     = alphaNum
      , Tok.opLetter        = oneOf ":!#%"  -- add this line
      }