2
valeriimВ свое время, захотевши написать простой компилятор мат.выражений, но не будучи знакомым с теорией синтаксического анлиза, грамматического разбора и т.д., я "изобрел" простой парсер, специально для нужд разбора простых формул. Как потом оказалось, дизайн парсера оказался не таким уж и плохим. Фактически, это обычный рекурсивно-нисходящий парсер, но свернутый в одну функцию.
Выглядит он примерно так:
Код:
tree expr(int p)
{
string op; tree left;
if(p>maxp) return term();
left=expr(p+1);
if(!check(p, op)) return left;
if(right(op))
left=infix(p, op, left);
else
do left=infix(p+1, op, left) while(check(p, op));
return left;
}
}
Здесь
p обозначает приоритет оператора (начиная с нуля);
maxp -- соответсвенно максимално возможный приоритет; функция
check принимает приоритет и ссылку на буфер, проверяет является ли текущая лексема оператором и если является, то совпадает ли его приоритет с данным, потом копирует оператор в указанный буфер и считывает следующую лексему (в случае удачи) и возвращает результат проверки; фукция
infix, как нетрудно догадаться, обрабатывает бинарный оператор указанного приоритета, т.е. конструирует соответствующий узел дерева; функция
term обрабатывает скобки и унарные (префиксные/постфиксные операторы); ну и наконец, функция
right просто проверяет оператор на правую ассоциативность.
Как может выглядеть код для
infix(p, op, left)? Он должен создать новый узел с меткой
op, левой подветкой которого станет переданная ветка, а правая ветка будет получена вызовом
expr(p). Возможный вид
infix'а:
Код:
tree infix(int p, char *op, tree left)
{
return mknode(op, left, expr(p));
}
Функция
term очень проста, но места занимает очень много. Начать её писать можно так:
Код:
tree term()
{
if(match("("))
{
t=expr(0);
expect(")");
return t;
}
switch(token.kind)
{
case tk_number: t=mknode(token.value); next_token(); return t;
...
Функция
match проверяет, совпадает ли текст текущей лексемы с данным и если совпадает, то считывает следующую лексему и возвращает истину. Функция
expect проводит почти такую-же проверку, но при несовпадении генерирует ошибку (и если парсер допускает восстановление после ошибок, то
expect может считывать новую лексему в любом случае). Что означает
next_token() надеюсь понятно (да, ещё и лексический анализатор придется написать, например в виде конечного автомата).
В общем, как-то так. Это проверенная работающая схема. Осталось только правильно распечатать построенное синтаксическое дерево; собственно в этом задание-то и заключалось. Наверное код будет примерно таким:
Код:
display(tree AST)
{
switch(AST->node_kind)
{
nk_op:
display(AST->left);
display(AST->right);
display(AST->left)
print(AST->op);
display(ASR->right);
break;
nk_funccall:
for(i=0; i<AST->argc; i++) display(AST->argv[i]);
print(AST->id);
print("(");
for(i=0; i<AST->argc; i++)
{
if(i) print(", ");
display(AST->argv[i]);
}
print(")");
...
Точно не знаю, как он должен выглядет, но, думаю, достаточно немного поэкспериментировать, чтобы это понять.
Подход с построением синтаксического дерева оправдан за счет необходимости дублировать выводимые на печать операнды при печати объемлющего их выражения. Т.е. в выражении
(a+b)*(c+d) сначала печатаем
(a+b), потом
(c+d), а потом все выражение, т.е. содержимое скобок печатается по нескольку раз. Навреное уж лучше один раз построить дерево, чем хранить все промежуточные куски строк (хотя ведь достаточно хранить позиции и длины подстрок, но генерация дерева хороша в образовательном смысле :) ).
P.S.: Я считаю, что для таких заданий прекрасно подходит техника синтаксического разбора с использованием комбинаторов парсеров, вроде хаскелевского parsec'а, boost::spirit для C++, или даже reparse для js. Это наиболее красивый способ дословного перевода нфбн-грамматики в программный код парсера. Обычно к таким библиотекам неизменно прилагается пример реализации калькулятора, вот его то и нужно переделать для генерации синтаксического дерева, которое потом нужно будет правильно распечатать, см. выше.
P.P.S.: Насчет запятой и правоассоциативности ничего не понял. Чем запятая отличается от других операторов? И при чем тут правоассоциативность(с каких пор запятая правоассоциативна)? И что особого в правой ассоциативности? Обычно, при прямом построении рекурсивно-нисходящего парсера, могут возникать проблемы, но не для право-, а для левоассоциативных операций (функция сразу же вызывает саму себя рекурсивно, что приводит к зацикливанию); правда это никакая не проблема и все исправляется простыми циклами...