ТЗ или о чем пойдет речь
Данный пост будет, как правило, интересен студентам, так как подобное задание было получено в качестве лабораторной работы по дисциплине «Лингвистические основы информатики».
Итак, давайте же рассмотрим техническое задание подробнее. Что нам требуется? А требуется нам создать анализатор, который будет разбивать заданный текст на языке Java по классам — ключевым словам, идентификаторам, операторам, пунктуаторам (сепараторам) и т.п., и выводить результат работы в таблицу. Таблица будет содержать следующие столбцы:
- Токен
- Спецификатор
- Описание
- Позиция
- Длинна
Напомню, что токен — это последовательности символов в лексическом анализе в информатике, соответствующий лексеме.
Спецификатор описывает к какому классу относится токен. То есть, например, для токена «boolean» в таблице выведется «Keywords».
Ну описание, позицию и длину описывать, я думаю, не стоит.
Вроде бы задание понятно. Теперь разобьем его на подзадачи.
0) Изначально я бы посоветовал изучить спецификацию языка, для которого вы будете писать анализатор. Далее нам нужно:
1) Загрузить массив данных о наших ключевых словах, операторах и пунктуаторах, так как они уникальны.
2) Распарсить заданный текст на токены и определить их классы. (Распарсить — то же самое, что и разобрать, т.е. выбрать эти элементы из текста в переменные)
3) Занести данные о токенах в массив и отсортировать его.
4) Вывести данные в таблицу.
Определимся с выбором
Каким же образом мы можем распарсить наш текст? Вообще метода 2:
- При помощи оператора case. Долго, нудно и не интересно. А если серьезно, то способ слишком объемен и сложен в реализации. То есть для каждого токена Вам нужно будет составить отдельное значение для переменной. Для одних только ключевых слов придется делать 52 (!) блока проверки условия. Я молчу о проверке ошибок литералов и комментариев. Вердикт — отсеиваем.
- И второй, более простой способ, использование регулярных выражений. Просто, быстро и удобно. Лично я использовал библиотеку 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/
Добавить комментарий