CodeNet / Приложения / Алгоритмы / Разбор выражений. Компиляторы и интерпретаторы. / Основы компиляции
Разбор снизу-вверх. Сдвиг-свертка.
Простое и операторное предшествование.
Рассмотрим разбор снизу-вверх, при котором промежуточные вы-
воды перемещаются по дереву по направлению к корню. Если считы-
вать символы цепочки слева направо, то дерево разбора будет
выглядеть следующим образом:
-S--
/ \
/-x¬ \
/ ¦ \
--w-+b--u-
Промежуточный вывод имеет вид xbu, где x - цепочка терминалов и
нетерминалов, из которой выводится просмотренная часть терми-
нальной цепочки w, bu - непросмотренная часть терминальной це-
почки, b - очередной символ. Чтобы продолжить разбор, можно ли-
бо добавить символ b к просмотренной части цепочки (выполнить
так называемый "сдвиг"), либо выделить в конце x такую цепочку
z (x=yz), что к z можно применить одно из правил грамматики B:z
и заменить x на цепочку yB (выполнить так называемую "свер-
тку"):
-S-- -S--
/ \ / \
/-x-b¬ \ /yB¬ \
/ ¦ \ / ¦ \
--w--b+-u- --w-+b--u-
После сдвига После свертки
Если свертку применять только к последним символам x, то мы
будем получать правые выводы цепочки. Такой разбор получил наз-
вание LR, где символ L (Left,левый) относится к просмотру це-
почки слева направо, а R (Right, правый) относится к получаемым
выводам. Упражнение: докажите, что все правые выводы находятся
во взаимно-одназначном соотвествии с последовательностями сдви-
гов и сверток.
Пример LR-разбора цепочки aabb в соответствии с грамматикой
S : SaSb | e. Символ _ указывает на границу между просмотренной
и непросмотренной частями цепочки: _aabb : S_aabb : Sa_abb :
SaS_abb : SaSa_bb : SaSaS_bb : SaSaSb_b : SaS_b : SaSb_ : S_
Последовательность операций сдвига и свертки существенна -
если бы в приведенном примере первой операцией был бы сдвиг,
разбор не удалось бы довести до конца. Поэтому для детерминиро-
ванного разбора требуется в каждый момент выбирать между сдви-
гом и сверткой (и различными правилами свертки). В курсе будут
рассмотрены три способа выбора: простое и операторное предшес-
твования и LR(k)-разбор.
Грамматики простого предшествования
Грамматика называется обратимой, если в ней нет двух правил
с одинаковыми правыми частями. Напомним, что грамматика называ-
ется приведенной, если в ней нет e-правил, бесполезных символов
и циклов.
Зададимся целью ввести на множестве терминальных и нетерми-
нальных символов три отношения (так называемые "отношения пред-
шествования") (будем обозначать их <', =' и >' ) так, чтобы в
момент, когда требуется свертка цепочки z, отношения между сим-
волами построенной части вывода (цепочки x в обозначениях из
начала лекции) и очередным символом b были следующими:
<' или =' <' =' =' >'
L----------+--------+---+---------
y z b u
Если удастся построить такие отношения, то LR-разбор для обра-
тимой грамматики можно проводить очень просто:
- сделать сдвиг, если последний символ x <' b или последний
символ x =' b
- сделать свертку, если последний символ x >' b, при этом
правая часть правила z заключена между отношениями <' и
>'.
Грамматика называется грамматикой простого предшествования,
если она приведенная, обратимая и между любыми двумя терминала-
ми или нетерминалами выполняется не более одного отношения
предшествования.
Практически отношения предшествования можно вычислить следу-
ющим образом (X,Y - терминалы или нетерминалы, A,B,C-нетермина-
лы, y - терминал, ... - любая цепочка (м.б. пустая)):
X =' Y, если в грамматике есть правило A : ...XY...
X <' Y, если в грамматике есть правило A : ...XB...
и B :: Y...
X >' y, если в грамматике есть правило A : ...By...
или A : ...BC...
и B :: ...X
и C :: y...
Вычисляя отношения предшествования для грамматики S:aSSb|c,
получим: a='S, S='S, S='b, {a,S} <' {a,c}, {b,c} >' {a,b,c}.
Эта грамматика является грамматикой простого предшествования.
На практике иногда удобно считать, что в начале и конце це-
почки языка стоят символы-ограничители #. Для них отношения
предшествования определяются так:
# <' X, если S :: X...
X >' #, если S :: ...X
В нашем примере {b,c} >' #, # <' {a,c}.
Отметим, что отношения <',>',=' не похожи на обычные арифме-
тические отношения <, >, = : =' не является отношением эквива-
лентности, <' и >' не обязательно транзитивны.
Отношения предшествования удобно занести в матрицу, в кото-
рой строки и столбцы помечены терминалами и нетерминалами грам-
матики. Упражнение: заполните эту матрицу. Что значит ситуация,
в которой между символами не выполняется ни одного из отношений
предшествования?
Грамматики операторного предшествования
Если в правилах приведенной обратимой грамматики не встреча-
ются рядом два нетерминала, говорят, что грамматика является
операторной. Классический пример - грамматика арифметических
формул. Для таких грамматик можно вычислять отношения пред-
шествования только на множестве терминальных символов. Для это-
го модифицируем правила вычисления отношений предшествования:
a =' b, если в грамматике имеется правило A : ...ab...
или A : ...aBb...
a <' b, если в грамматике имеется правило A : ...aB...
и B :: b...
или B :: Cb...
a >' b, если в грамматике имеется правило A : ...Bb...
и B :: ...a
или B :: ...aC
# <' a, если в грамматике имеется вывод S :: Ca...
или S :: a...
a >' #, если в грамматике имеется вывод S :: ...aC
или S :: ...a
Если между любыми двумя терминалами выполняется не более од-
ного отношения предшествования, операторная грамматика называ-
ется грамматикой операторного предшествования.
Вычислим матрицу операторного предшествования для грамматики
арифметических формул:
----T------------¬
¦ ¦ ( a * + ) #¦
S : S + T | T +---+------------+
T : T * E | E ¦ ) ¦ > > > >¦
E : (S) | a ¦ a ¦ > > > >¦
¦ * ¦ < < > > > >¦
¦ + ¦ < < < > > >¦
¦ ( ¦ < < < < = ¦
¦ # ¦ < < < < ¦
L---+-------------
Эти отношения позволяют определять терминалы, входящие в
правую часть сворачиваемого правила, но не нетерминалы. Однако,
при практическом разборе формул нет необходимости различать не-
терминалы S, T и E. Они были введены только для придания грам-
матике однозначности и учета приоритета и ассоциативности опе-
раций. Теперь, получив отношения предшествования, можно вновь
заменить эти искуственно введенные нетерминалы на S:
S : S+S | S*S | (S) | a
и получить разбор, обрабатывая все нетерминалы одинаково.
Линеаризация матрицы предшествования
Для компактного хранения матрицы предшествования часто можно
использовать следующий прием. По матрице M[n][n], элементы ко-
торой принимают только три значения (<' =' >'), попытаемся
построить два целочисленных вектора f и g:
M[i][j] равно >', если f[i]>g[j]
M[i][j] равно <', если f[i]<g[j]
M[i][j] равно =', если f[i]=g[j]
Для получения этих векторов используется следующий метод:
- построить ориентированный граф, содержащий n вершин типа F
и n вершин типа G;
- построить ребро графа F[i]->G[j], если i >' j
- построить ребро графа F[i]<-G[j], если i <' j
- склеить вершины F[i] и G[j], если i =' j
Если полученный граф циклический, то линеаризация невозможна.
Иначе положить f[i] равным длине самого длинного пути из F[i],
g[i] равным длине самого длинного пути из G[i].
Применив этот метод для построенной матрицы операторного
предшествования, получим:
символ # a + * ( )
f 0 4 2 4 0 4
g 0 5 1 3 5 0
Еще один компилятор формул (операторное предшествование)
Для реализации компилятора формул в польскую постфиксную за-
пись свяжем с каждой сверткой действие:
a : Число или имя - напечатать число или имя
S : a | (S) - ничего не делать
S : S*S | S+S - напечатать знак операции
Построенный частичный вывод x будем хранить в стеке:
%{ /* Коды лексем и метасимволов */
#define EF 0 /* конец файла */
#define ID 1 /* идентификатор или число */
#define PLUS 2 /* '+' */
#define MULT 3 /* '*' */
#define LP 4 /* '(' */
#define RP 5 /* ')' */
%}
%%
"+" { return(PLUS); }
"*" { return(MULT); }
"(" { return(LP); }
")" { return(RP); }
[A-Z0-9]+ { printf("%s ",yytext); return(ID); }
\n { }
. { printf("%s: ",yytext); error(0); }
%%
static char *view[] = { /* Постфиксное представление терминалов */
"", /* конец файла */ "", /* идентификатор */
"+ ", /* '+' */ "* ", /* '*' */
"", /* '(' */ "", /* ')' */
};
/* Линеаризованная матрица операторного предшествования */
static f[] = { 0, 4, 2, 4, 0, 4, };
static g[] = { 0, 5, 1, 3, 5, 0, };
main() {
register c, d;
stinit(); stpush(EF);
do {
c = yylex();
while ( f[sttop()] > g[c] ) {
do {
d = stpop();
printf ("%s",view[d]);
} while ( f[sttop()]==g[d] );
}
stpush(c);
} while(c!=EF);
}
/* Реализация стека терминалов */
#define MAXSTK 100
static int stack[MAXSTK], *stptr;
stinit() { stptr = stack+MAXSTK; }
stpush(e) {
if ( stptr==stack ) error(1);
*--stptr = e;
}
sttop() { stcheck(); return(*stptr); }
stpop() { stcheck(); return(*stptr++); }
stcheck() { if (stptr==stack+MAXSTK) error(1); }
static char *ermsg[] = { /* Тексты сообщений об ошибках */
/* 00 */ "ошибочный символ", /* 01 */ "отказ",
};
error(e) { printf ("%s\n", ermsg[e]); exit(1); }
yywrap() { return(1); }
Задачи для решения на ЭВМ:
1. Переделайте компилятор так, чтобы он не допускал ошибочных
(т.е. не порождаемых грамматикой) цепочек.
2. Добавьте в компилятор операцию возведения в степень (право-
ассоциативная операция "^" с наивысшим приоритетом).
3. Переделайте компилятор формул в интерпретатор.
4. Добавьте в компилятор операцию "унарный минус".
