Мегапарсек: не удалось разобрать арифметическую строку
Я работаю над небольшим парсером, использующим Мегапарсек, и пытаюсь разобрать арифметику.
-- Arithmetic expressions
data Aexp = N Num
| V Var
| Mult Aexp Aexp
| Add Aexp Aexp
| Sub Aexp Aexp
deriving (Show, Eq, Read)
arithParser :: Parser Aexp
arithParser = V <$> strParser
<|> N <$> numParser
<|> Mult <$> arithParser <* tok "*" <*> arithParser
--boolParser :: Parser Bexp
strParser :: Parser Var
strParser = tok "\"" *> some (noneOf ("\n\r\"=[]{},:")) <* tok "\""
numParser :: Parser Num
numParser = (some (oneOf ['0' .. '9']) >>= return . read) <* whitespace
Если я запускаю команду Parse arithParser "5*5" "5*5"
это просто возвращает Right (N 5)
куда он должен вернуться Mult(N 5) (N 5)
, Из-за приоритета в arithParser. Но если я изменю порядок, то, похоже, он заходит в бесконечный цикл и вылетает.
Не уверен, что я делаю не так, любая помощь будет оценена.
2 ответа
Парсек пытается левый вариант <|>
прежде чем он попробует правильный. Если левая альтернатива будет успешной, она не будет беспокоить правую. Так что в этом случае, когда кормили вход 5*5
Процесс Parsec выглядит следующим образом:
- Пытаться
V <$> strParser
,strParser
начинается сtok "\""
, но входная строка не начинается с'"'
такstrParser
выходит из строя. - Пытаться
N <$> numParser
,numParser
успешно разбирает число 5, поэтому Parsec просто возвращаетN 5
, - Готово! Не нужно пробовать третий вариант.
Таким образом, мы можем попытаться исправить этот парсер, переместив Mult
вариант до верха, завернутый в try
так что он может вернуться и попробовать numParser
или же strParser
если вход оказывается не умножением.
arithParser :: Parser Aexp
arithParser = try (Mult <$> arithParser <* tok "*" <*> arithParser)
<|> N <$> numParser
<|> V <$> strParser
У этого парсера есть другая, более тонкая проблема. Давайте пройдемся по ступеням, как указано выше.
- Пытаться
try (Mult <$> arithParser <* tok "*" <*> arithParser)
, Этот парсер начинается сarithParser
так рекурсивно звониarithParser
, - Пытаться
try (Mult <$> arithParser <* tok "*" <*> arithParser)
, Этот парсер начинается сarithParser
так рекурсивно звониarithParser
, - Пытаться
try (Mult <$> arithParser <* tok "*" <*> arithParser)
, Этот парсер начинается сarithParser
так рекурсивно звониarithParser
, - ...
Это бесконечный цикл. Parsec не может обработать леворекурсивные грамматики. Вы должны спроектировать свой синтаксический анализатор так, чтобы он использовал по крайней мере один токен перед рекурсивным вызовом. Один из распространенных способов сделать это - "сгладить" вашу грамматику:
expr, term :: Parser AExp
expr = do
n <- term
rest <- optional $ tok "*" *> expr
return $ maybe n (Mult n) rest
term = N <$> numParser
<|> V <$> strParser
<|> parenthesised expr
parenthesised = between (char '(') (char ')')
Здесь я разделил парсер на один, который разбирает произвольный expr
- а term
необязательно сопровождается символом умножения и умножением expr
- и тот, который анализирует один term
s - числа, строки и выражения в скобках. Рекурсивные вызовы expr
все в порядке - один внутри expr
происходит только после того, как вы проанализировали term
(который всегда потребляет вход) и тот, что внутри term
происходит только после того, как вы проанализировали открывающую скобку.
Обратите внимание, что expr
имеет структуру, подобную списку: она анализирует одну вещь, за которой, возможно, следуют многие вещи. В общем, вы должны думать о парсерах, потребляющих линейный входной поток входных токенов, поэтому неудивительно, что парсеры в форме списка имеют тенденцию быть более эффективными, чем древовидные.
Text.Megaparsec.Expr
Модуль содержит функции, которые упаковывают этот шаблон и анализируют выражения с произвольными правилами приоритета и исправления.
expr = makeExprParser term [[InfixR $ tok "*" $> Mult]]
Типы лгут вам: когда вы определяете рекурсивный парсер p
вы не можете использовать p
сама где хочешь! Вы должны сначала скопировать часть ввода, чтобы гарантировать, что вы делаете успехи. Иначе Хаскелл действительно с радостью войдет в бесконечный цикл.
Как правило, эта проблема решается путем определения различных "ярусов" выражений и предоставления только "более простых" или "более сложных" скобок в лево-рекурсивных позициях (поскольку сопоставление с открытыми скобками заставляет вас пробираться через часть строка ввода).
Например, грамматика для ваших выражений будет преобразована (от самой простой до самой сложной):
<Literal> ::= [0-9]+
<Var> ::= [a-zA-Z]+
<Base> ::= '(' <Expr> ')' | <Var> | <Literal>
<Factor> ::= <Base> | <Base> '*' <Factor>
<Expr> ::= <Factor> | <Factor> '+' <Expr> | <Factor> '-' <Expr>
Вот где сияет тотальный язык: поскольку типы должны быть абсолютно честными, когда дело доходит до завершения, становится буквально невозможным написать эти плохо ведущие себя рекурсивные парсеры. Проверка типов говорит вам, что вам нужно найти другой способ распознавания терминов вашего языка.
Например, исправление комбинатора fixpoint, которое я использую в своей библиотеке комбинаторов общего синтаксического анализа, не имеет типа (a -> a) -> a
а точнее (игнорируя смешные скобки) (□ a → a) → a
что точно мешает вам использовать рекурсивный вызов, прежде чем вы добились определенного прогресса. Вы все еще можете написать парсер для Expr, но средство проверки типов здесь, чтобы предупредить вас, когда вы делаете незаконный ход.