Лексический анализатор языка Java, созданный в среде Delphi

от автора

ТЗ или о чем пойдет речь

Данный пост будет, как правило, интересен студентам, так как подобное задание было получено в качестве лабораторной работы по дисциплине «Лингвистические основы информатики».
Итак, давайте же рассмотрим техническое задание подробнее. Что нам требуется? А требуется нам создать анализатор, который будет разбивать заданный текст на языке Java по классам — ключевым словам, идентификаторам, операторам, пунктуаторам (сепараторам) и т.п., и выводить результат работы в таблицу. Таблица будет содержать следующие столбцы:

  • Токен
  • Спецификатор
  • Описание
  • Позиция
  • Длинна

Напомню, что токен — это последовательности символов в лексическом анализе в информатике, соответствующий лексеме.
Спецификатор описывает к какому классу относится токен. То есть, например, для токена «boolean» в таблице выведется «Keywords».
Ну описание, позицию и длину описывать, я думаю, не стоит.
Вроде бы задание понятно. Теперь разобьем его на подзадачи.
0) Изначально я бы посоветовал изучить спецификацию языка, для которого вы будете писать анализатор. Далее нам нужно:
1) Загрузить массив данных о наших ключевых словах, операторах и пунктуаторах, так как они уникальны.
2) Распарсить заданный текст на токены и определить их классы. (Распарсить — то же самое, что и разобрать, т.е. выбрать эти элементы из текста в переменные)
3) Занести данные о токенах в массив и отсортировать его.
4) Вывести данные в таблицу.

Определимся с выбором

Каким же образом мы можем распарсить наш текст? Вообще метода 2:

  1. При помощи оператора case. Долго, нудно и не интересно. А если серьезно, то способ слишком объемен и сложен в реализации. То есть для каждого токена Вам нужно будет составить отдельное значение для переменной. Для одних только ключевых слов придется делать 52 (!) блока проверки условия. Я молчу о проверке ошибок литералов и комментариев. Вердикт — отсеиваем.
  2. И второй, более простой способ, использование регулярных выражений. Просто, быстро и удобно. Лично я использовал библиотеку RegExpr с сайта RegExpr Studio.
Этап 1. Заполняем массивы данных

Итак, нам нужно создать массивы, в которых будут наши ключевые слова, операторы, пунктуаторы и их описание. Вы можете создать уже заполненные массивы в самой программе, но я, лично, что бы не перегружать код и упростить редактирование данных, делал выгрузку из 3х файлов и буду объяснять именно на этом примере.
Создаем 3 файла. У меня это «keywords.txt», «operator.txt» и «separator.txt». Заполняем их в соответствии с их названием. Каким образом заполняем? Пишем (вставляем), например, ключевое слово и через пробел его описание. Как это выглядит?
_____________
abstract modificator
boolean type
break CTRLcycle
byte type
case selection
catch exception
char type
______________
Ну и так далее. То есть тоже самое мы делаем для операторов и сепараторов. Сделали? Отлично! Едем дальше.

Теперь нам все эти файлы надо выгрузить в массив, да не просто в массив, а в динамический массив записей, в которых первый элемент будет токеном, а второй его описанием. Реализовано это при создании формы следующим образом:

  while not EOF(FKeyw) do     begin       readln(fkeyw,s);       SetLength(KWRD,i+1);  //уведичиваем массив на 1 элемент       KWRD[i].token:=copy(s,1,(pos(' ',s)-1)); // копируем в токен все, что до пробела       KWRD[i].spec:=copy(s,(pos(' ',s)+1),length(s)); //а в описание все, что после       inc(i);    end; 

, где FKeyw — файловая переменная для файла Keywords.txt, а KWRD — это массив записей для ключевых слов, объявленный, как ГЛОБАЛЬНАЯ переменная.
Аналогичным образом заполняются массивы OPRT и PNCTR. Ну по названию видно какой массив к какому файлу относится.
На этом, собственно, первый этап заканчивается.

Этап 2. Самый длинный.

Модуль RegExpr уже скачан и подключен к программе. Что дальше? А дальше мы проходим по ссылочке ниже и читаем синтаксис наших регулярных выражений, так как копировать всю информацию с этого сайта не имеет смысла.
Синтаксис регулярных выражений RegExpr

Предположим (да так оно и есть), что программа работает по нажатию на кнопку.
Теперь прописываем в разделе описания переменных для Button1Click:
r:Tregexpr;
Теперь создаем наш объект (после begin естессно):
R:=TRegExpr.Create;
Далее кидаем на форму объект RichEdit. Чем обусловлен выбор именно его, а не, например, Memo, я объясню чуть позже. Организуем выгрузку из него в строковую переменную txt, но после каждой строки будем добавлять символ #13.
for xx:=0 to richedit1.Lines.Count do
txt:=txt+richedit1.Lines[xx]+#13;

Создание регулярного выражения, в соответствии с его синтаксисом осуществляется при помощи выражения
r.Expression:='(МАСКА)’;

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

 r.Expression:='([a-zA-Z_]+[a-zA-Z0-9_]*)'; //задается маска       if r.exec(txt) then              //если такой элемент найден, то       repeat        begin          ma:=r.Match[1];          //сюда записывается найденый токен          for i:=1 to length(KWRD)-1 do            begin              s:=KWRD[i].token;              if s=ma then      //если такой токен найден в массиве ключевых слов, то                begin                  inc(j);                  setlength(it,j+1);                   it[j].token:=s;         //записать в итоговый массив сам токен                  it[j].cl:='Keywords';   // записать в итоговый массив класс токена                  it[j].spec:=KWRD[i].spec;  //записать описание                  it[j].ps:=r.MatchPos[1];  //записать позицию в тексте                  it[j].len:=r.MatchLen[1];  //записать длину                  delete(txt, it[j].ps, it[j].len);                  ss:='';                  for xx:=1 to r.MatchLen[1] do                    ss:=ss+' ';                                      //здесь произвелась замена этого токена на пробелы,                   insert(ss,txt,r.MatchPos[1]);         // что бы не потерять позицию при поиске следующих регулярных выражений                  break;                end else                                                   //если такой токен не найден в массиве, то                 if i=length(KWRD)-1 then                   begin                    inc(j);                    setlength(it,j+1);                   //тут опять все записываем                    it[j].token:=ma;                    it[j].cl:='Identificator';                    it[j].spec:='-//-';                        //ну а какое описание у идентификатора может быть?!                    it[j].ps:=r.MatchPos[1];                    it[j].len:=r.MatchLen[1];                    delete(txt, it[j].ps, it[j].len);                    ss:='';                    for xx:=1 to r.MatchLen[1] do                      ss:=ss+' ';                    insert(ss,txt,r.MatchPos[1]);                    break;                  end;             end;          end;       until not r.ExecNext;                            // до тех пор. пока не проверит все найденные токены 

Таким же образом у нас ищутся операторы и пунктуаторы. Ничего сложного, думаю сами разберетесь.
Теперь, что касается комментариев. Комментарии у нас не должны определяться в принципе. То есть то, что будет /*Записано так*/ или
// будет записано вот так
не должно у нас определяться нашим анализатором вообще.
Ниже я приведу код только для двойного слеша (//), для закрывающихся комментариев по образу и подобию постарайтесь сделать сами.

aa:=0;       r.Expression:='(//)'; //находит двойной слеш     while aa=0 do     begin     aa:=1;     qqq:=0;     if r.exec(txt) then       repeat          if qqq=1 then             break;         for ps:=r.MatchPos[1]+2 to length(txt) do           begin             if txt[ps]=#13 then                             //находит конец строки (помните мы добавляли? )               begin                 ln:=(ps)-r.MatchPos[1];                 delete(txt,r.MatchPos[1],ln+1);                 ss:='';                  for xx:=1 to ln+1 do                    ss:=ss+' ';                                                             insert(ss,txt,r.MatchPos[1]);             //заменяет на пробелы                  aa:=0;                  qqq:=1;                  break;               end;           end;       until not r.ExecNext;     end; 

PROFIT.
Ниже привожу ПОРЯДОК нахождения токенов в программе и комментарии.

1) Находим комментарии вида /* */
2) Находим комментарии вида //
3) Находим символьные литералы вида ‘С’, ‘\t’, ‘\n’ (что бы в Delphi заэкранировать апостроф надо поставить их два )
4) Находим строковые литералы вида «Строковый литерал»
5) Находим вещественные числа с экспонентой (несколько выражений)
6) Находим вещественные числа с точкой ( r.Expression:='([1-9]*[\.][0-9]+[fFdD]?|[0-9]+[\.][1-9]*[fFdD]?)’; );
7) Находим шестнадцатеричные числа вида 0x3AF04B ( r.Expression:='(0[xX][0-9|a-f|A-F]+[l|L]?)’; )
8) Находим двоичные числа вида 0b01001010 ( r.Expression:='([0][b][01]+)’; )
9) Находим восьмеричные числа вида 038423 ( r.Expression:='([0][0-9]+[l|L]?)’; )
10) Находим десятичные числа ( r.Expression:='([0-9]+[L|l]*)’; )
11) Находим ключевые слова и идентификаторы
12) Находим операторы (выражение длинное, но составить легко. Прост скопируйте все операторы из файла и экранируйте некоторые символы с помощью значка обратного слеша \)
13) Находим сепараторы ( r.Expression:='([\(\)\{\}\[\]\;\.\,])’; )

Ну вот и самое сложное позади. Едем дальше.

Этап 3. Сортируем массив по значению позиции

Сортировок массивов в интернете полно, так что я просто приведу код наиболее простого и эффективного способа.

 repeat         bool:=false;         for i:=0 to j-1 do         begin           if it[i].ps>it[i+1].ps then             begin               temp:=it[i];  //temp - переменная записи                it[i]:=it[i+1];               it[i+1]:=temp;               bool:=true;             end;         end;       until not(bool); 
Этап 4. Заполняем таблицу

Заполняем мы компонент StringGrid. Думаю сложностей с ним не возникнет, но код, на всякий случай, приведу.

  stringgrid1.RowCount:=j+1;     stringgrid1.cells[0,0]:='Токен';     stringgrid1.cells[1,0]:='Класс';     stringgrid1.cells[2,0]:='Описание';     stringgrid1.cells[3,0]:='Позиция';     stringgrid1.cells[4,0]:='Длина';     for m:=1 to length(it)-1 do       begin         stringgrid1.cells[0,m]:=it[m].token;         stringgrid1.cells[1,m]:=it[m].cl;         stringgrid1.cells[2,m]:=it[m].spec;         stringgrid1.cells[3,m]:=inttostr(it[m].ps);         stringgrid1.cells[4,m]:=inttostr(it[m].len);       end; 
Заключение

Что бы программа имела совсем вылизанный вид, Вам потребуется вывести в отдельное поле ошибки, связанные с незакрытыми комментариями или литералами, а так же сделать обработку ситуаций, когда в литерале будут знаки комментариев.
В качестве бонуса привожу код, который позволит Вам, щелкая по токену в таблице, выделять его в RichEdit (для чего, собственно, он нам и был нужен ).

procedure TForm1.StringGrid1SelectCell(Sender: TObject; ACol, ARow: Integer;  var CanSelect: Boolean);   var ps,ln,k:integer; begin   ps:=strtoint(stringgrid1.Cells[3,Arow]);   ln:=strtoint(stringgrid1.Cells[4,Arow]);   richedit1.SelStart:=ps-1;   richedit1.SelLength:=ln;   richedit1.SetFocus; end; 

Внешний вид программы:

Удачной работы и спасибо за внимание!

ссылка на оригинал статьи http://habrahabr.ru/post/167069/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *