Haskell Parsec: трогательные операторы - PullRequest
1 голос
/ 19 марта 2019

У меня есть логический язык, определенный следующим BNF.

 formula ::= true
           | false
           | var
           | formula & formula
           | [binder] formula

 binder  ::= var 
           | $var

По сути, это позволяет использовать такие формулы, как x & true, [x]x и [$x](x & true). Семантика здесь не важна; но важно то, что эти квадратные скобки выражаются перед формулами, а внутри этих квадратных скобок идентификаторам может предшествовать или не предшествовать знак доллара ($). Теперь я использовал библиотеку Parsec на Haskell, чтобы помочь мне создать синтаксический анализатор для этого языка, подробно описано ниже.

module LogicParser where

import System.IO
import Control.Monad
import Text.ParserCombinators.Parsec
import Text.ParserCombinators.Parsec.Expr
import Text.ParserCombinators.Parsec.Language
import qualified Text.ParserCombinators.Parsec.Token as Token

-- Data Structures
data Formula = LVar String
             | TT
             | FF
             | And Formula Formula
             | Bound Binder Formula
             deriving Show

data Binder = BVar String
             | FVar String
             deriving Show

-- Language Definition
lang :: LanguageDef st
lang =
    emptyDef{ Token.identStart      = letter
            , Token.identLetter     = alphaNum
            , Token.reservedOpNames = ["&", "$", "[", "]"]
            , Token.reservedNames   = ["tt", "ff"]
            }


-- Lexer for langauge
lexer = 
    Token.makeTokenParser lang


-- Trivial Parsers
identifier     = Token.identifier lexer
keyword        = Token.reserved lexer
op             = Token.reservedOp lexer
roundBrackets  = Token.parens lexer
whiteSpace     = Token.whiteSpace lexer

-- Main Parser, takes care of trailing whitespaces
formulaParser :: Parser Formula
formulaParser = whiteSpace >> formula

-- Parsing Formulas
formula :: Parser Formula
formula = andFormula
        <|> formulaTerm

-- Term in a Formula
formulaTerm :: Parser Formula
formulaTerm = roundBrackets formula
            <|> ttFormula
            <|> ffFormula
            <|> lvarFormula
            <|> boundFormula

-- Conjunction
andFormula :: Parser Formula
andFormula = 
    buildExpressionParser [[Infix (op "&" >> return And) AssocLeft]] formulaTerm

-- Bound Formula
boundFormula :: Parser Formula
boundFormula = 
    do  op "["
        v <- var
        op "]"
        f <- formulaTerm
        return $ Bound v f

-- Truth
ttFormula :: Parser Formula
ttFormula = keyword "tt" >> return TT

-- Falsehood
ffFormula :: Parser Formula
ffFormula = keyword "ff" >> return FF

-- Logical Variable
lvarFormula :: Parser Formula
lvarFormula =
    do  v <- identifier
        return $ LVar v

-- Variable
var :: Parser Binder
var = try bvar <|> fvar

-- Bound Variable
bvar :: Parser Binder
bvar =
    do  op "$"
        v <- identifier
        return $ BVar v

-- Free Variable
fvar :: Parser Binder
fvar =
    do  v <- identifier
        return $ FVar v


-- For testing
main :: IO ()
main = interact (unlines . (map stringParser) . lines)

stringParser :: String -> String
stringParser s = 
    case ret of
        Left e ->  "Error: " ++ (show e)
        Right n -> "Interpreted as: " ++ (show n)
    where 
        ret = parse formulaParser "" s

Моя проблема заключается в следующем. Когда оператор знака доллара ($) касается квадратной скобки, я получаю сообщение об ошибке, тогда как, если я добавляю пробел, анализатор работает нормально:

enter image description here

Как мне заставить синтаксический анализатор распознавать [$x](x & true)? Обратите внимание, что у него нет проблем с тем, что & касается его операндов, только когда два оператора [ и $ касаются.

Ответы [ 3 ]

1 голос
/ 19 марта 2019

Я думаю, вам не нравятся квадратные скобки в качестве операторов. Я бы попробовал это:

  1. удалить "[" и "]" из Token.reservedOpNames
  2. добавьте еще один парсер в свои тривиальные парсеры: squareBrackets = Token.brackets lexer
  3. измените вашу boundFormula функцию на:

    boundFormula =
        do v <- squareBrackets var
           f <- formulaTerm
           return $ Bound v f
    
1 голос
/ 19 марта 2019

Я не очень знаком с токеновой стороной Parsec, но из его документации Я думаю, вам нужно предоставить opLetter и, возможно, opStart, поскольку вы предоставляете reservedOp:

opLetter :: ParsecT s u m Char    

Этот синтаксический анализатор должен принимать любые допустимые хвостовые символы операторов. Обратите внимание, что этот синтаксический анализатор должен быть даже определен, если язык не поддерживает определяемые пользователем операторы, или иначе синтаксический анализатор reservedOp не будет работать правильно.

В частности, по умолчанию opLetter не включает [ или ], поэтому он ведет себя хаотично, когда вы думаете, что один из них должен быть операционным.

0 голосов
/ 19 марта 2019

Вот как я бы написал ваш парсер с Megaparsec ( документы , учебник ):

import Text.Megaparsec
import qualified Text.Megaparsec.Char as C
import qualified Text.Megaparsec.Char.Lexer as L
import Control.Monad.Combinators.Expr
import Data.Void

type Parser = Parsec Void String

data Formula = LVar String
             | TT
             | FF
             | Not Formula -- Added to demonstrate `Prefix` of `makeExprParser`
             | And Formula Formula
             | Bound Binder Formula
             deriving Show

data Binder = BVar String
            | FVar String
            deriving Show

Мегапарсек также имеет makeExprParser, но он перемещен в отдельный parser-combinators пакет:

formula :: Parser Formula
formula = makeExprParser term operators
  where
    term = choice
      [ TT <$ symbol "true"
      , FF <$ symbol "false"
      , LVar <$> var
      , Bound <$> brackets binder <*> parens formula
      ]

    binder = choice
      [ BVar <$> (C.char '$' >> var)
      , FVar <$> var
      ]

    var = lexeme $ some C.letterChar

    operators :: [[Operator Parser Formula]]
    operators =
      [ [ Prefix (Not <$ symbol "¬") ]
      , [ InfixL (And <$ symbol "&") ]
      ]

Некоторые баллы:

  • Используйте по возможности аппликативный стиль (<$>, <$, $>).
  • Мегапарсек переименовал некоторые комбинаторы, такие как many1 в some. Есть учебник, Переключение с Parsec на Megaparsec , который помогает переходу. (Это может быть немного устаревшим; я отправил пиар в процессе ответа на этот вопрос.)
  • Вам не нужно try, когда BVar s и FVar s являются взаимоисключающими для первого символа, $. Достаточно просто указать сначала парсер BVar. Поскольку вы не найдете других допустимых выражений, начинающихся с $, сбойный синтаксический анализатор, использовавший $, не является проблемой.
  • Ваша грамматика ничего не говорит о буквальных скобках или буквальных скобках после скобок. Таким образом, для разбора "[$x](x & true)" вам необходимо добавить явные скобки в грамматику, например

    formula ::= ... | '(' formula ')'
    

    или как

    formula ::= ... | '[' binder ']' '(' formula ')'
    

    если они там только разрешены. Я пошел с последним, но это, вероятно, неправильно.

Продолжение,

lexeme :: Parser a -> Parser a
lexeme = L.lexeme spaceConsumer

symbol :: String -> Parser String
symbol = L.symbol spaceConsumer

spaceConsumer :: Parser ()
spaceConsumer = L.space C.space1 empty empty

brackets, parens :: Parser a -> Parser a
brackets = between (symbol "[") (symbol "]")
parens = between (symbol "(") (symbol ")")

Некоторые последние очки,

  • Используйте between для переноса парсера в brackets или braces.
  • Парсеры токенов Parsec немного сложны, потому что они требуют от вас указать, что такое токен, что такое пробел и так далее. Синтаксические анализаторы лексем Megaparsec имеют некоторую сложность, но вы можете опустить не относящиеся к делу части (например, строчные комментарии и блочные комментарии), используя empty :: Alternative f => f a.
  • Пробелы в комбинаторах синтаксического анализа сложны. Убедитесь, что все парсеры являются или лексемными парсерами (например, symbol "foo", lexeme $ some C.letterChar) или комбинациями лексемных парсеров. Если вы используете lexeme для чего-то, что уже является синтаксическим анализатором лексемы, вы делаете что-то не так, и если вы забудете использовать lexeme для чего-то, вы рискуете запретить пробелы в местах, где вы хотите это разрешить.

    Я не использовал lexeme вокруг C.char '$'.

  • Всегда есть угловые чехлы, например пробел в начале:

    > parseTest formula " [$x](x & true) "
    1:1:
      |
    1 |  [$x](x & true) 
      | ^^^^^
    unexpected " [$x]"
    expecting "false", "true", '[', '¬', or letter
    

    Если вы хотите полностью утверждать, что ваш синтаксический анализатор допускает пробелы во всех нужных местах, вы можете создать «уродливый принтер», который для произвольных деревьев синтаксиса вставляет произвольные пробелы, где это разрешено. В таком случае ваша собственность заключается в том, что синтаксический анализ уродливого синтаксического дерева должен привести к тому же синтаксическому дереву.

  • Ошибки синтаксического анализа в мегапарсеке выглядят очень красиво.

    Они могут выглядеть лучше, если вы используете оператор <?> (также доступен в Parsec).

...