Написание рекурсивного потомка парсера для моей грамматики
Я должен построить выражение, используя Recursive Descendent Parser Builder.
Вот моя грамматика:
----RULES----
<cond> → <termb> [OR <termb>]*
<termb>→<factb>[AND <factb>]*
<factb>→<expr> RELOP <expr> | NOT <factb> | OPAR <cond> CPAR
<expr> → [PLUS | MINUS] <term> [(PLUS <term>) | (MINUS <term>)]*
<term> → <termp> [(MULT <termp>) | (DIV <termp>) | (REM <termp>)]*
<termp> → <fact> [POWER <fact>]*
<fact> → ID | NUM | OPAR1 <expr> CPAR1
----TERMINALS----
ID → ("A" | ... | "Z" | "a" | ...| "z") [("A"| ... | "Z" | "a" | ...| "z" | "0" | ... | "9")]*
NUM → ("0" | ... | "9") [("0" | ... | "9")]*
OPAR → "("
CPAR → ")"
OPAR1 → "["
CPAR1 → "]"
RELOP → EQ | NEQ | GT | GE | LT | LE
EQ → "= ="
NEQ → "!="
GT → ">"
GE → ">="
LT → "<"
LE → "<="
POWER → "^"
DIV → "/"
REM → "%"
MULT → "*"
MINUS → "−"
PLUS → "+"
AND → “and” or “&&”
OR → “or” or “||”
NOT → “not” or “!”
Теперь у меня есть составная структура с одним классом для каждого терминала, и я пытаюсь построить ее, следуя правилам приведенной выше грамматики.
Идея состоит в том, что мне нужен метод для каждого правила грамматики, и каждый из этих методов должен строить свою часть дерева выражений.
Хотя он работает с более простой грамматикой (например, грамматика, которая допускает только логические выражения), у меня есть некоторые проблемы с этим.
Основная проблема исходит от expr
Правило, которое заставляет меня работать даже с унарными версиями моих + и -, позволяя такие выражения +3-4
, Это требует, чтобы я выяснил, когда операнд должен считаться унарным, а когда - двоичным!
Вот код моего класса Builder. Пожалуйста, обратите внимание, что что-то на итальянском, но я объяснил ВСЕ, даже мои проблемы, используя комментарии. Также обратите внимание, что есть одна строка, в которой я использовал псевдокод, так что эта НЕ компилируется!
package espressioniCondizionali;
import espressioniCondizionali.espressioni.*;
/**
*
* @author Stefano
*/
public class Builder6 {
/**
* This one is used just to parse the input String.
* It has a prossimoSimbolo() method (something like a nextSymbol()) that returns a String with the next Symbol in the String
*/
private GestoreInput gestoreInput;
/**
* Espressione is the Composite structure that represents and expression
*/
private Espressione root = null;
private String symbol = null;
public Builder6(GestoreInput gestoreInput) {
this.gestoreInput = gestoreInput;
}
public Espressione build() {
buildCond();
return root;
}
private void buildCond() {
buildTermb();
//I'm unsing a class named Simboli that holds every terminal symbol of my grammar. Symbol names are the same defined in the grammar.
while (symbol.equalsIgnoreCase(Simboli.OR1) || symbol.equalsIgnoreCase(Simboli.OR2)) {
Or or = new Or();
or.setLeft(root);
buildTermb();
or.setRight(root);
root = or;
}
}
private void buildTermb() {
buildFactb();
while (symbol.equalsIgnoreCase(Simboli.AND1) || symbol.equalsIgnoreCase(Simboli.AND2)) {
And and = new And();
and.setLeft(root);
buildFactb();
and.setRight(root);
root = and;
}
}
private void buildFactb() {
buildExpr();
if (symbol.equalsIgnoreCase(Simboli.EQ) || symbol.equalsIgnoreCase(Simboli.NEQ) || symbol.equalsIgnoreCase(Simboli.GT) || symbol.equalsIgnoreCase(Simboli.LT) || symbol.equalsIgnoreCase(Simboli.LE) || symbol.equalsIgnoreCase(Simboli.GE)) {
Operatore op = null;
switch (symbol) {
case Simboli.EQ: {
op = new Eq();
break;
}
case Simboli.NEQ: {
op = new Neq();
break;
}
case Simboli.GT: {
op = new Gt();
break;
}
case Simboli.GE: {
op = new Ge();
break;
}
case Simboli.LT: {
op = new Lt();
break;
}
case Simboli.LE: {
op = new Le();
break;
}
default: {
//Symbol not recognized, abort!
throw new RuntimeException("\"" + symbol + "\" non è un simbolo valido.");
}
}
op.setLeft(root);
buildExpr();
op.setRight(root);
root = op;
} else if (symbol.equalsIgnoreCase(Simboli.NOT1) || symbol.equals(Simboli.NOT2)) {
Not not = new Not();
buildFactb();
not.setRight(root);
root = not;
} else if (symbol.equalsIgnoreCase(Simboli.OPAR)) {
buildCond();
if (!symbol.equalsIgnoreCase(Simboli.CPAR)) { //If there's no closgin square bracket it means that our expression is not well formed
throw new RuntimeException("Espressione non valida. Attesa una \")\", trovato \"" + symbol + "\"");
}
}
}
private void buildExpr() {
Operatore op = null;
if (symbol != null) { //Let's check if our expression starts with a + or a -
if (symbol.equalsIgnoreCase(Simboli.PLUS)) {
op = new Plus();
} else if (symbol.equalsIgnoreCase(Simboli.MINUS)) {
op = new Minus();
}
}
buildTerm();
//If our op is still null, we didn't found a + or a - so our operand will be a binary one
//If op != null, our + or - is unary and we've got to manage it! A unary operand doesn't have a left son!
if (op != null) {
op.setRight(root);
root = op;
}
//Since we can have something like -3+2+s-x we've got to check it
while (symbol.equalsIgnoreCase(Simboli.PLUS) || symbol.equalsIgnoreCase(Simboli.MINUS)) {
Operatore op1 = null; //We define a new Operatore that will be a + or a -
switch (symbol) {
case Simboli.PLUS: {
op1 = new Plus();
break;
}
case Simboli.MINUS: {
op1 = new Minus();
break;
}
}
/*
* Here comes a BIG problem. We used the first if/else to check if
* our unary operand was at the beginning of the string, but now
* we've got to see if our current operand is either binary or
* unary! That's because we can have both a==-1+d and a==3-1+d. In
* the first case, the - is unary, while is binary in the second.
* So, how do we choose it?
*
* EXAMPLE : (-a>2 || -b-12>0)
* This one will be evaluated to -a > 2 || -12 > 0 that's clearly wrong!
* -b is missing before -12. That's because the -12 is used as unary
* and so it won't have a left child (the -b part)
*/
//--PSEUDOCODE
if (op1 is not unary) {
op1.setLeft(root);
}
//--END PSEUDOCODE
//CURRENT IMPLEMENTATION OF THE PSEUDOCODE PART
if (root != null && (root.getClass().equals(Num.class) || root.getClass().equals(Id.class))) { //It is binary if the previous value is a constant or a variable but not if it is an operand!
op1.setLeft(root);
}
//END OF CURRENT IMPLEMENTATION OF THE PSEUDOCODE PART
//Setting the right child must be done in both cases
buildTerm();
op1.setRight(root);
root = op1;
}
}
private void buildTerm() {
buildTermp();
while (symbol.equalsIgnoreCase(Simboli.MULT) || symbol.equalsIgnoreCase(Simboli.DIV) || symbol.equalsIgnoreCase(Simboli.REM)) {
Operatore op = null;
switch (symbol) {
case Simboli.MULT: {
op = new Mult();
break;
}
case Simboli.DIV: {
op = new Div();
break;
}
case Simboli.REM: {
op = new Rem();
break;
}
}
op.setLeft(root);
buildTermp();
op.setRight(root);
root = op;
}
}
private void buildTermp() {
buildFact();
while (symbol.equalsIgnoreCase(Simboli.POWER)) {
Power p = new Power();
p.setLeft(root);
buildFact();
p.setRight(root);
root = p;
}
}
private void buildFact() {
symbol = gestoreInput.prossimoSimbolo();
if (symbol.equalsIgnoreCase(Simboli.OPAR1)) { //Sottoespressione
buildExpr();
if (!symbol.equalsIgnoreCase(Simboli.CPAR1)) { //Se non c'è una parentesi chiusa vuol dire che l'espressione non valida
throw new RuntimeException("Espressione non valida. Attesa una \"]\", trovato \"" + symbol + "\"");
}
} else if (symbol.matches("[A-Za-z][A-Za-z | 0-9]*")) { //Nome di una variabile!
root = new Id(symbol);
symbol = gestoreInput.prossimoSimbolo();
} else if (symbol.matches("[0-9]*")) { //E' una costante!
root = new Num(symbol);
symbol = gestoreInput.prossimoSimbolo();
}
}
}
Известные проблемы:
(a<=b && c>1) || a==4
оценивает a <= b && c > 1
a==[-4]
оценивает a == a - 4
-4+3>c-2
оценивает +3 > c - 2
Вероятно, есть еще некоторые ошибки, но они являются наиболее распространенными.
Итак, вот мои вопросы:
Прежде всего, вы думаете, что этот код имеет некоторые логические проблемы? Я имею в виду, это делает то, что говорит грамматика, или я сделал что-то действительно неправильно? Как бы вы исправить expr
способ заставить его работать с одинарными и двоичными + или - операндами?
Если мой код полностью неверен, есть ли кто-нибудь, кто может помочь мне написать рабочую версию? Как видно из названия класса, это шестая реализация, которую я написал, и я действительно не знаю, что делать дальше!
Благодарю.
1 ответ
Шаг 1: Подумайте о грамматике по-другому
Я думаю, что у вас есть проблемы с реализацией вашего парсера рекурсивного спуска, потому что ваша грамматика очень сложна. Произвольная итерация, представленная [ ... ]*
формы трудно обернуть голову вокруг. Попробуйте думать об этом так:
<cond> → <termb> <cond-tail>
<cond-tail> → OR <termb> <cond-tail> | EPSILON
<termb> → <factb> <termb-tail>
<termb-tail> → AND <factb> <termb-tail> | EPSILON
<factb> → <expr> RELOP <expr> | NOT <factb> | OPAR <cond> CPAR
<expr> → <unary-op> <term> <expr-tail>
<unary-op> → PLUS | MINUS | EPSILON
<expr-tail> → ((PLUS <term>) | (MINUS <term>)) <expr-tail> | EPSILON
<term> → <termp> <term-tail>
<term-tail> → ((MULT <termp>) | (DIV <termp>) | (REM <termp>)) <term-tail> | EPSILON
<termp> → <fact> <termp-tail>
<termp-tail> → POWER <fact> <termp-tail> | EPSILON
<fact> → ID | NUM | OPAR1 <expr> CPAR1
EPSILON → ""
Эта грамматика такая же, как вы опубликовали, но я разделил [ ... ]*
правила в свои собственные нетерминалы. Для этого я добавил EPSILON
правило, которое позволяет нетерминалу совпадать на ""
(пустая строка). Это в основном то же самое, что и ваш [ ... ]
правила, которые могут или не могут действительно соответствовать чему-то. Правило epsilon - это всего лишь последняя альтернатива, которая гласит: "Прекратите пытаться сопоставить сейчас". Например, если вы в настоящее время разбираете для cond-tail
тогда вы бы сначала проверить на OR
на входе. Если вы не видите OR
Затем вы переходите к следующей альтернативе, которая EPSILON
, Это означает, что больше нет OR
условия в этой условной цепочке, так что это cond-tail
соответствует пустой строке.
Вы можете еще больше упростить грамматику, сгруппировав операторы в expr-tail
а также term-tail
:
<cond> → <termb> <cond-tail>
<cond-tail> → OR <termb> <cond-tail> | EPSILON
<termb> → <factb> <termb-tail>
<termb-tail> → AND <factb> <termb-tail> | EPSILON
<factb> → <expr> RELOP <expr> | NOT <factb> | OPAR <cond> CPAR
<expr> → <unary-op> <term> <expr-tail>
<unary-op> → PLUS | MINUS | EPSILON
<expr-tail> → (PLUS | MINUS) <term> <expr-tail> | EPSILON
<term> → <termp> <term-tail>
<term-tail> → (MULT | DIV | REM) <termp> <term-tail> | EPSILON
<termp> → <fact> <termp-tail>
<termp-tail> → POWER <fact> <termp-tail> | EPSILON
<fact> → ID | NUM | OPAR1 <expr> CPAR1
EPSILON → ""
Теперь вам нужно вернуться и реорганизовать свое решение, чтобы добавить новые методы для каждого из новых правил. (Вместо этого вы можете оставить код новых правил в исходных правилах, но, по крайней мере, четко пометьте различные разделы комментариями, чтобы помочь вам отслеживать, какие части кода делают что.)
Теперь должно быть очень очевидно, в каких случаях +
а также -
унарные операторы, потому что они будут проанализированы вашим новым buildUnaryOp
метод (соответствующий новому unary-op
правило в грамматике).
Что касается нового *-tail
правила, вы можете реализовать их строго рекурсивно, или вы можете реализовать их с помощью цикла внутри тела функции, если вы беспокоитесь о том, что ваш стек будет вытеснен из длинных выражений.
Шаг 2: Исправьте отслеживание вашего состояния
Это ваша главная проблема прямо здесь:
/**
* Espressione is the Composite structure that represents and expression
*/
private Espressione root = null;
private String symbol = null;
Вы используете поля уровня экземпляра для хранения состояния вашего анализатора рекурсивного спуска. Я никогда не видел парсер рекурсивного спуска, методы которого имеют тип возврата void
- по крайней мере, если на самом деле создается абстрактное синтаксическое дерево. Пример в Википедии имеет void
return-types, но это потому, что все, что он делает - проверяет ввод и отбрасывает результаты, а не строит AST из ввода.
Поскольку вы сохраняете свое состояние в полях уровня класса, поле перезаписывается при каждом вызове одного из buildExpr()
и вы теряете свои данные. Это моя теория в любом случае. Вы можете проверить это, сделав длинную строку expr
как "1+2+3+4+5"
, Это должно закончить тем, что бросило все условия на фронт.
Я бы порекомендовал построить парсер функционально (без использования каких-либо .set*()
методы), где каждый build*()
Метод возвращает проанализированный узел из AST. Это будет включать в себя изменение всех ваших конструкторов для принятия дочерних узлов. Вы все еще можете использовать мутацию вместо этого, если это более понятно для вас. Вот переделал buildCond
:
private Espressione buildCond() {
Espressione leftHandSide = buildTermb();
if (symbol.equalsIgnoreCase(Simboli.OR1) || symbol.equalsIgnoreCase(Simboli.OR2)) {
Or or = new Or();
or.setLeft(leftHandSide);
or.setRight(buildTermb());
return or;
}
return leftHandSide;
}
Вот что нового build()
будет выглядеть так:
public Espressione build() {
return buildCond();
}
Если вы переделаете остальную часть своего кода, чтобы построить дерево таким образом (вместо того, чтобы использовать поля уровня экземпляра для хранения вашего состояния), тогда он должен работать намного лучше.
Удачи!