Основы конструирования компиляторов

         

Синтаксический анализ для T-грамматик


Обычно код генерируется из некоторого промежуточного языка с довольно жесткой структурой. В частности, для каждой операции известна ее размерность, то есть число операндов, большее или равное 0. Операции задаются терминальными символами, и наоборот - будем считать все терминальные символы знаками операций. Назовем

грамматики, удовлетворяющие этим ограничениям, T-грамматиками. Правая часть каждой продукции в Т-грамматике есть правильное префиксное выражение, которое может быть задано следующим определением:

  • Операция размерности 0 является правильным префиксным выражением;
  • Нетерминал является правильным префиксным выражением;
  • Префиксное выражение, начинающееся со знака операции размерности n > 0, является правильным, если после знака операции следует

    n правильных префиксных выражений;

  • Ничто другое не является правильным префиксным выражением.

Образцы, соответствующие машинным командам, задаются правилами грамматики (вообще говоря, неоднозначной). Генератор кода анализирует входное префиксное выражение и строит одновременно все возможные деревья разбора. После окончания разбора выбирается дерево с наименьшей стоимостью. Затем по этому единственному оптимальному дереву генерируется

код.

Для T-грамматик все цепочки, выводимые из любого нетерминала A, являются префиксными выражениями с фиксированной арностью операций. Длины всех выражений из входной цепочки a1...an можно предварительно вычислить (под длиной выражения имеется ввиду длина подстроки, начинающейся с символа кода операции и заканчивающейся последним символом, входящим в выражение для этой операции). Поэтому можно проверить, сопоставимо ли некоторое правило с подцепочкой ai...ak входной цепочки a1...an,

проходя слева-направо по ai...ak. В процессе прохода по цепочке предварительно вычисленные длины префиксных выражений используются для того, чтобы перейти от одного терминала к следующему терминалу, пропуская подцепочки, соответствующие нетерминалам правой части правила.

Цепные правила не зависят от операций, следовательно, их необходимо проверять отдельно.
Применение одного цепного правила может зависеть от применения другого цепного правила. Следовательно, применение

цепных правил необходимо проверять до тех пор, пока нельзя применить ни одно из цепных правил. Мы предполагаем, что в грамматике нет циклов в применении цепных правил. Построение всех вариантов анализа для T-грамматики дано ниже в алгоритме 9.1. Тип Titem в алгоритме 9.1 ниже служит для описания ситуаций (т.е. правил вывода и позиции внутри правила). Тип Tterminal - это тип терминального символа грамматики, тип Tproduction - тип для правила вывода.

 



Алгоритм 9.1

 Tterminal a[n];  
 setofTproduction r[n];  
 int l[n];  // l[i] - длина a[i]-выражения  
 Titem h;   // используется при поиске правил,  
            // сопоставимых с текущей подцепочкой  
 // Предварительные вычисления  
 Для каждой позиции i вычислить длину a[i]-выражения l[i];  
 // Распознавание входной цепочки  
   for (int i=n-1;i>=0;i-){  
     for (каждого правила A -> a[i] y из P){  
       j=i+1;  
       if (l[i]>1)  
       // Первый терминал a[i] уже успешно сопоставлен  
         {h=[A->a[i].y];  
          do // найти a[i]y, сопоставимое с a[i]..a[i+l[i]-1]  
           {Пусть h==[A->u.Xv];  
            if (X in T)  
             if (X==a[j]) j=j+1; else break;  
            else // X in N  
             if (имеется X->w in r[j]) j=j+l[j]; else break;  
            h=[A->uX.v];  
            //перейти к следующему символу  
       }  
          while (j!=i+l[i]);  
         } // l[i]>1  
       if (j==i+l[i]) r[i]=r[i] + { (A->a[i]y) }  
     } // for по правилам A -> a[i] y  
     // Сопоставить цепные правила  
     while (существует правило C->A из P такое, что  
           имеется некоторый элемент (A->w) в r[i]  
           и нет элемента (C->A) в r[i])  
       r[i]=r[i] + { (C->A) };  
   } // for по i  
   Проверить, принадлежит ли (S->w) множеству r[0];

<


Множества r[i] имеют размер O(|P|). Можно показать, что алгоритм

имеет временную и емкостную сложность O(n).

Рассмотрим вновь пример рис. 9.15. В префиксной записи приведенный фрагмент программы записывается следующим образом:

      = + a x + @ + + b y @ + i z 5

На рис. 9.18 приведен результат работы алгоритма. Правила вычисления стоимости приведены в следующем разделе. Все возможные выводы входной цепочки (включая оптимальный) можно построить, используя таблицу l длин префиксных выражений и таблицу r применимых правил.



Рис. 9.18:
Пусть G - это T-грамматика. Для каждой цепочки z из L(G) можно построить абстрактное синтаксическое дерево соответствующего выражения (рис. 9.15). Мы можем переписать алгоритм так, чтобы он принимал на входе абстрактное синтаксическое дерево выражения, а не цепочку. Этот вариант алгоритма приведен ниже. В этом алгоритме дерево выражения обходится сверху вниз и в нем ищутся поддеревья, сопоставимые с правыми частями правил из G. Обход дерева осуществляется процедурой

PARSE. После обхода поддерева данной вершины в ней применяется процедура MATCHED, которая пытается найти все образцы, сопоставимые поддереву данной вершины. Для этого каждое правило-образец разбивается на компоненты в соответствии с встречающимися в нем операциями. Дерево обходится справа налево только для того, чтобы иметь соответствие с порядком вычисления в алгоритме 9.1. Очевидно, что можно обходить дерево вывода и слева направо.

Структура данных, представляющая

вершину дерева, имеет следующую форму:

  struct Tnode {  
    Tterminal op;  
    Tnode * son[MaxArity];  
    setofTproduction RULEs;  
   };

В комментариях указаны соответствующие фрагменты алгоритма 9.1.

 

Алгоритм 9.2

Tnode * root;  

 
bool MATCHED(Tnode * n, Titem h)  
 { bool matching;  
   пусть h==[A->u.Xvy], v== v_1 v_2 ... v_m, m=Arity(X);  
   if (X in T)// сопоставление правила  
     if (m==0)  // if l[i]==1  
        if (X==n->op)   //if X==a[j]  
          return(true);  
        else  
          return(false);  
     else // if l[i]>1  
      if (X==n->op) //if X==a[j]  
        {matching=true;  
         for (i=1;i         matching=matching &&   //while (j==i+l[i])  
                MATCHED(n->son[i-1],[A->uXv'.v_i v"y]);  
         return(matching);  //h=[A->uX.v]  
            }  
      else  
        return(false);  
    else // X in N поиск подвывода  
      if (в n^.RULEs имеется правило с левой частью X)  
        return(true);  
      else  
        return(false);  
  }  

 
void PARSE(Tnode * n)  
 {  
  for (i=Arity(n->op);i>=1;i-)  
  // for (i=n; i>=1;i-)  
     PARSE(n->son[i-1]);  
  n->RULEs=EMPTY;  
  for (каждого правила A->bu из P такого, что b==n->op)  
     if (MATCHED(n,[A->.bu]))   //if (j==i+l[i])  
      n->RULEs=n->RULEs+{(A->bu)};  
 // Сопоставление цепных правил  
  while (существует правило C->A из P такое, что  
         некоторый элемент (A->w) в n->RULEs  
         и нет  элемента   (C->A) в n->RULEs)  
    n->RULEs=n->RULEs+{(C->A)};

 
 }  

 
    Основная программа  

 
 // Предварительные вычисления  
    Построить дерево выражения для входной цепочки z;  
    root = указатель дерева выражения;  
 // Распознать входную цепочку  
    PARSE(root);  
    Проверить, входит ли во множество root->RULEs  
    правило с левой частью S;

Выходом алгоритма является дерево выражения для z, вершинам которого сопоставлены применимые правила. С помощью такого дерева можно построить все выводы для исходного префиксного выражения.


Содержание раздела